library(ggplot2)
library(SingleCellExperiment)
library(tidyverse)
library(igraph)
library(scran)

devtools::load_all("~/milo/miloR/")

Testing with random sampling

Simulate linear trajectory

Using dyntoy, I simulate a scRNA-seq data with cells forming a linear trajectory (no branches) with 5000 cells and 10 main states (milestones).

set.seed(42)
dataset <- generate_dataset(
  model = model_linear(num_milestones = 10),
  num_cells = 5000,
  num_features = 5000
)

|===============================================================================     | 95% ~0 s remaining     
|=================================================================================   | 97% ~0 s remaining     
|=================================================================================== | 99% ~0 s remaining     
gex <- as.matrix(dataset$expression)
branches <- dataset$prior_information$groups_id
  
## Dimensionality reduction
pca <- prcomp_irlba(gex, n=50, scale.=TRUE, center=TRUE)
X_pca = pca$x[, c(1:30)]

I assign cells to simulated biological conditions and replicates, that we will use for differential abundance testing. For each of the \(M\) clusters, I assign different proportions of cells to condition A or condition B, while simulating proportionate mixing between replicates.

Construct a Milo object

sim_milo
class: Milo 
dim: 5000 5000 
metadata(0):
assays(2): counts logcounts
rownames(5000): G1 G2 ... G4999 G5000
rowData names(0):
colnames(5000): C1 C2 ... C4999 C5000
colData names(4): group_id condition replicate sample
reducedDimNames(1): PCA
altExpNames(0):
neighbourhoods dimensions(1): 0
neighbourhoodCounts dimensions(2): 1 1
neighbourDistances dimensions(2): 1 1
graph names(0):
neighbourhoodIndex names(1): 0

Build KNN-graph

sim_milo <- buildGraph(sim_milo, k = 20, d = 30)
Constructing kNN graph with k:20
Retrieving distances from 20 nearest neighbours

Make neighbourhoods

Using sampling of 10% random points

Count cells within neighbourhoods

Milo: Simulations for discrete cases

Introduction

Generating simulations and then using the thymus ageing data to test differential abundance testing on a graph/tree.

library(ggplot2)
library(edgeR)
library(igraph)
library(SingleCellExperiment)
library(scran)
library(scater)
library(irlba)
library(ggthemes)
library(ggsci)
library(cydar)
library(mvtnorm)
library(umap)
library(reshape2)

Simulation 1

I’ll start by simulating 3 clouds of points in \(\mathbb{R}^{n}\), each consisting of points from 2 pools, A & B. Each cloud of points will be composed of:

  • A: 10%, B: 90%
  • A: 90%, B: 10%
  • A: 50%, B: 50%

I will then perform a PCA on the matrix of these points and construct a KNN-graph, mimicing the standard workflow of many scRNA-seq analyses.

set.seed(42)
r.n <- 1000
n.dim <- 50
block1.cells <- 1200
# select a set of eigen values for the covariance matrix of each block, say 50 eigenvalues?
block1.eigens <- sapply(1:n.dim, FUN=function(X) rexp(n=1, rate=abs(runif(n=1, min=0, max=50))))
block1.eigens <- block1.eigens[order(block1.eigens)]
block1.p <- qr.Q(qr(matrix(rnorm(block1.cells^2, mean=4, sd=0.01), block1.cells)))
block1.sigma <- crossprod(block1.p, block1.p*block1.eigens)
block1.gex <- abs(rmvnorm(n=r.n, mean=rnorm(n=block1.cells, mean=2, sd=0.01), sigma=block1.sigma))
block2.cells <- 1200
# select a set of eigen values for the covariance matrix of each block, say 50 eigenvalues?
block2.eigens <- sapply(1:n.dim, FUN=function(X) rexp(n=1, rate=abs(runif(n=1, min=0, max=50))))
block2.eigens <- block2.eigens[order(block2.eigens)]
block2.p <- qr.Q(qr(matrix(rnorm(block2.cells^2, mean=4, sd=0.01), block2.cells)))
block2.sigma <- crossprod(block2.p, block2.p*block2.eigens)
block2.gex <- abs(rmvnorm(n=r.n, mean=rnorm(n=block2.cells, mean=4, sd=0.01), sigma=block2.sigma))
block3.cells <- 1250
# select a set of eigen values for the covariance matrix of each block, say 50 eigenvalues?
block3.eigens <- sapply(1:n.dim, FUN=function(X) rexp(n=1, rate=abs(runif(n=1, min=0, max=50))))
block3.eigens <- block3.eigens[order(block3.eigens)]
block3.p <- qr.Q(qr(matrix(rnorm(block3.cells^2, mean=4, sd=0.01), block3.cells)))
block3.sigma <- crossprod(block3.p, block3.p*block3.eigens)
block3.gex <- abs(rmvnorm(n=r.n, mean=rnorm(n=block3.cells, mean=5, sd=0.01), sigma=block3.sigma))
sim1.gex <- do.call(cbind, list("b1"=block1.gex, "b2"=block2.gex, "b3"=block3.gex))
sim1.pca <- prcomp_irlba(t(sim1.gex), n=50, scale.=TRUE, center=TRUE)
pairs(sim1.pca$x[, c(1:5)])

I’ll use the reduced dimensions here to compute a KNN-graph and visualise it using a Fructerman-Reingold layout.

set.seed(42)
sim1.knn <- buildKNNGraph(x=sim1.pca$x[, c(1:30)], k=21, d=NA, transposed=TRUE)
sim1.fr.layout <- layout_with_fr(sim1.knn)
plot(sim1.knn, layout=sim1.fr.layout, vertex.frame.color='skyblue', vertex.color='skyblue', vertex.label.color='black', 
     vertex.label.family='Helvetica', edge.color='grey60', vertex.label.cex=0.9,
     vertex.label.dist=1, edge.arrow.size=0.2)

Also a UMAP layout.

set.seed(42)
stem.ta.umap <- umap(sim1.pca$x[, c(1:30)],
                     n_components=2,
                     n_neighbors=21, metric='euclidean',
                     init='random', min_dist=0.1)
plot(stem.ta.umap$layout, col=c(rep("red", block1.cells), rep("skyblue", block2.cells), rep("orange", block3.cells)),
     xlab="UMAP 1", ylab="UMAP 2")

Within each of these clouds of points I will randomly label in the proportions above.

set.seed(42)
block1.cond <- rep("A", block1.cells)
block1.a <- sample(1:block1.cells, size=floor(block1.cells*0.9))
block1.b <- setdiff(1:block1.cells, block1.a)
block1.cond[block1.b] <- "B"
block2.cond <- rep("A", block2.cells)
block2.a <- sample(1:block2.cells, size=floor(block2.cells*0.05))
block2.b <- setdiff(1:block2.cells, block2.a)
block2.cond[block2.b] <- "B"
block3.cond <- rep("A", block3.cells)
block3.a <- sample(1:block3.cells, size=floor(block3.cells*0.5))
block3.b <- setdiff(1:block3.cells, block3.a)
block3.cond[block3.b] <- "B"
meta.df <- data.frame("Block"=c(rep("B1", block1.cells), rep("B2", block2.cells), rep("B3", block3.cells)),
                      "Condition"=c(block1.cond, block2.cond, block3.cond),
                      "Replicate"=c(rep("R1", floor(block1.cells*0.33)), rep("R2", floor(block1.cells*0.33)), 
                                    rep("R3", block1.cells-(2*floor(block1.cells*0.33))),
                                    rep("R1", floor(block2.cells*0.33)), rep("R2", floor(block2.cells*0.33)), 
                                    rep("R3", block2.cells-(2*floor(block2.cells*0.33))),
                                    rep("R1", floor(block3.cells*0.33)), rep("R2", floor(block3.cells*0.33)), 
                                    rep("R3", block3.cells-(2*floor(block3.cells*0.33)))))
meta.df <- cbind(meta.df, stem.ta.umap$layout)
colnames(meta.df) <- c("Block", "Condition", "Replicate", "UMAP1", "UMAP2")
# define a "sample" as teh combination of condition and replicate
meta.df$Sample <- paste(meta.df$Condition, meta.df$Replicate, sep="_")
meta.df$Vertex <- c(1:nrow(meta.df))
ggplot(meta.df, aes(x=UMAP1, y=UMAP2)) +
  geom_point(aes(colour=Block, shape=Replicate)) +
  theme_clean() +
  scale_colour_npg() +
  facet_wrap(~Condition) +
  guides(colour=guide_legend(override.aes=list(size=3)),
         shape=guide_legend(override.aes=list(size=3)))

Using these simulated data we can define lots of neighbourhoods across the graph to create a counts matrix of neighbourhoods vs conditions in a similar way as cydar. I’ll start with random vertices in the graph making up 5% of all points and extract the graph neighborhoods.

# randomly select vertices in the graph
n.hood <- 0.05
random.vertices <- sample(V(sim1.knn), size=floor(n.hood*length(V(sim1.knn))))
# loop over random vertices and count the number of cells in each
vertex.list <- sapply(1:length(random.vertices), FUN=function(X) neighbors(sim1.knn, v=random.vertices[X]))
hist(unlist(lapply(vertex.list, length)), 100, main="Histogram of neighbors", xlab="Neighbourhood size")

For each neighbourhood I’ll count the number of cells in each, determined by the experimental design, i.e. replicate, condition and block.

quant_neighbourhood <- function(graph, meta, sample.column='Sample', sample.vertices=0.25, seed=42){
  set.seed(seed)
  # define a set of vertices and neihbourhood centers - extract the neihbourhoods of these cells
  random.vertices <- sample(V(graph), size=floor(sample.vertices*length(V(graph))))
  vertex.list <- sapply(1:length(random.vertices), FUN=function(X) neighbors(graph, v=random.vertices[X]))
  
  count.matrix <- matrix(0L, ncol=length(unique(meta[, sample.column])), nrow=length(vertex.list))
  colnames(count.matrix) <- unique(meta[, sample.column])
  
  for(x in seq_along(1:length(vertex.list))){
    v.x <- vertex.list[[x]]
    for(i in seq_along(1:length(unique(meta[, sample.column])))){
      i.s <- unique(meta[, sample.column])[i]
      i.s.vertices <- intersect(v.x, meta[meta[, sample.column] == i.s, ]$Vertex)
      count.matrix[x, i] <- length(i.s.vertices)
    }
  }
  rownames(count.matrix) <- c(1:length(vertex.list))
  return(count.matrix)
}
# define a set of vertices and neihbourhood centers - extract the neihbourhoods of these cells
set.seed(42)
random.vertices <- sample(V(sim1.knn), size=floor(n.hood*length(V(sim1.knn))))
vertex.list <- sapply(1:length(random.vertices), FUN=function(X) neighbors(sim1.knn, v=random.vertices[X]))
count.matrix <- matrix(0L, ncol=length(unique(meta.df[, "Sample"])), nrow=length(vertex.list))
colnames(count.matrix) <- unique(meta.df[, "Sample"])
for(x in seq_along(1:length(vertex.list))){
  v.x <- vertex.list[[x]]
  for(i in seq_along(1:length(unique(meta.df[, "Sample"])))){
    i.s <- unique(meta.df[, "Sample"])[i]
    i.s.vertices <- intersect(v.x, meta.df[meta.df[, "Sample"] == i.s, ]$Vertex)
    count.matrix[x, i] <- length(i.s.vertices)
  }
}
rownames(count.matrix) <- c(1:length(vertex.list))
sim1.counts <- quant_neighbourhood(graph=sim1.knn, meta=meta.df, sample.column='Sample', sample.vertices=n.hood)
sample.meta <- data.frame("Condition"=c(rep("A", 3), rep("B", 3)),
                          "Replicate"=rep(c("R1", "R2", "R3"), 2))
sample.meta$Sample <- paste(sample.meta$Condition, sample.meta$Replicate, sep="_")
rownames(sample.meta) <- sample.meta$Sample
# sim1.model <- model.matrix(~ 0 + Condition, data=sample.meta)
sim1.model <- model.matrix(~ Condition, data=sample.meta)
head(sim1.model)
     (Intercept) ConditionB
A_R1           1          0
A_R2           1          0
A_R3           1          0
B_R1           1          1
B_R2           1          1
B_R3           1          1

I have a model matrix and counts matrix - let’s test edgeR on these.

sim1.dge <- DGEList(sim1.counts[, rownames(sim1.model)], lib.size=log(colSums(sim1.counts[, rownames(sim1.model)])))
sim1.dge <- estimateDisp(sim1.dge, sim1.model)
sim1.fit <- glmQLFit(sim1.dge, sim1.model, robust=TRUE)
# sim1.contrast <- makeContrasts(ConditionA - ConditionB, levels=sim1.model)
# sim1.res <- glmQLFTest(sim1.fit, contrast=sim1.contrast)
sim1.res <- as.data.frame(topTags(glmQLFTest(sim1.fit, coef=2), sort.by='none', n=Inf))
sim1.res$Sig <- as.factor(as.numeric(sim1.res$FDR <= 0.01))
sim1.res$Neighbourhood <- as.numeric(rownames(sim1.res))
# control the spatial FDR
qvals <- sim1.res$PValue
is.sig <- qvals <= 0.01
summary(is.sig)
   Mode   FALSE    TRUE 
logical      59     123 

This indicates that 123 of the neighbour hoods are significant before spatial FDR correction. The implementation in Cydar relies on having the median marker intensities for each hypersphere. What is the equivalent positional information here? What about drawing a new graph for each neighborhood using the igraph::induced_subgraph function, then calculate the connectivity of this new sub-graph as a measure of the neighborhood density?

# this creates a sub-graph for each of the random vertices
test.subgraphs <- lapply(1:length(random.vertices),
                         FUN=function(X) induced_subgraph(sim1.knn, vertex.list[[X]]))
# now loop over these sub-graphs to calculate the connectivity - this seems a little slow...
test.connect <- lapply(test.subgraphs, FUN=function(EG) vertex_connectivity(EG))

Vertex connectivity is slow to compute - edge connectivity might be faster and has the useful property that:

\[k(G) \leq \lambda(G) \] That is, the vertex connectivity \(k(G)\) is \(\leq\) than the edge connectivity \(\lambda(G)\). In this instance the edge-connectivity is an upper-bound on the neighborhood connectivity, and thus an upper bound on the density, leading to a slightly lower weighting for the spatial FDR adjustment.

# now loop over these sub-graphs to calculate the connectivity - this seems a little slow...
edge.connect <- lapply(test.subgraphs, FUN=function(EG) edge_connectivity(EG))

The edge-connectivity takes ~10% of the time. How do they compare?

plot(unlist(test.connect), unlist(edge.connect), main="Graph-connectivity measures", xlab="Vertex-connectivity", ylab="Edge-connectivity")

For the most part the vertex-connectivity and edge-connectivity for neighborhoods are in pretty good agreement. I’ll maybe include the option to use either as a function parameter.

graph_spatialFDR <- function(neighborhoods, graph, pvalues, connectivity='vertex'){
  # input a set of neighborhoods as a list of graph vertices
  # the input graph and the unadjusted GLM p-values
  #' neighborhoods: list of vertices and their respective neighborhoods
  #' graph: input kNN graph
  #' pvalues: a vector of pvalues in the same order as the neighborhood indices
  #' connectivity: character - edge or vertex to calculate neighborhood connectivity
  # Discarding NA pvalues.
  haspval <- !is.na(pvalues)
  if (!all(haspval)) {
      coords <- coords[haspval, , drop=FALSE]
      pvalues <- pvalues[haspval]
  }
    
  # define the subgraph for each neighborhood then calculate the vertex connectivity for each
  # this latter computation is quite slow - can it be sped up?
  subgraphs <- lapply(1:length(neighborhoods[haspval]),
                         FUN=function(X) induced_subgraph(graph, neighborhoods[haspval][[X]]))
  # now loop over these sub-graphs to calculate the connectivity - this seems a little slow...
  if(connectivity == "vertex"){
    connect <- lapply(subgraphs, FUN=function(EG) vertex_connectivity(EG))
  } else if(connectivity == "edge"){
    connect <- lapply(subgraphs, FUN=function(EG) edge_connectivity(EG))
  } else{
    errorCondition("connectivity option not recognised - must be either edge or vertex")
  }
  
  # use 1/connectivity as the weighting for the weighted BH adjustment from Cydar
  w <- 1/unlist(connect)
  # set Inf to 0
  w[is.infinite(w)] <- 0
  
  # Computing a density-weighted q-value.
  o <- order(pvalues)
  pvalues <- pvalues[o]
  w <- w[o]
  adjp <- numeric(length(o))
  adjp[o] <- rev(cummin(rev(sum(w)*pvalues/cumsum(w))))
  adjp <- pmin(adjp, 1)
  if (!all(haspval)) {
    refp <- rep(NA_real_, length(haspval))
    refp[haspval] <- adjp
    adjp <- refp
    }
  return(adjp)
}
start.time <- Sys.time()
sim1.spatialfdr <- graph_spatialFDR(neighborhoods=vertex.list, graph=sim1.knn, connectivity="edge",
                                    pvalues=sim1.res[order(sim1.res$Neighbourhood), ]$PValue)
end.time <- Sys.time()
connect.time <- end.time - start.time
sim1.res$SpatialFDR[order(sim1.res$Neighbourhood)] <- sim1.spatialfdr
qvals <- sim1.spatialfdr
is.sig <- qvals <= 0.01
summary(is.sig)
   Mode   FALSE    TRUE 
logical      62     120 

How do these compare to the standard FDR adjustment?

plot(-log10(sim1.res[order(sim1.res$Neighbourhood), ]$FDR),
     -log10(sim1.spatialfdr), main="FDR comparison", xlab="-log10 FDR", ylab="-log10 Spatial FDR")

In this instance there is not a huge difference. That seems to work fairly well. Does it make sense though? i.e. are the changes in the graph in the expected regions? I’ll follow the Cydar workflow for this and project the neighbourhoods into a reduced dimensional space.

neighborhood_expression <- function(neighborhoods, data.set){
  # I'll calculate the average value of each neighborhood for each of the n features in the data.set
  neighbour.model <- matrix(0L, ncol=length(neighborhoods), nrow=ncol(data.set))
  # neighbour.model <- sapply(1:length(neighborhoods), FUN=function(X) print(length(neighbour.model[, X])))
  for(x in seq_along(1:length(neighborhoods))){
    neighbour.model[neighborhoods[[x]], x] <- 1
  }
  
  neigh.exprs <- t(neighbour.model) %*% t(data.set)
  neigh.exprs <- t(apply(neigh.exprs, 2, FUN=function(XP) XP/colSums(neighbour.model)))
  return(neigh.exprs)
}
sim1.neighbour.exprs <- neighborhood_expression(vertex.list, sim1.gex)

Embed these hyperspheres with a PCA and UMAP.

sim1.neighbour.pca <- prcomp((t(sim1.neighbour.exprs)))
pairs(sim1.neighbour.pca$x[, c(1:5)])

set.seed(42)
neighbourhood.umap <- umap(sim1.neighbour.pca$x[, c(1:30)],
                     n_components=2,
                     n_neighbors=21, metric='euclidean',
                     init='random', min_dist=0.1)
plot(neighbourhood.umap$layout,
     xlab="UMAP 1", ylab="UMAP 2")

We can overlay the DA testing on these neighbourhoods.

neighbor.df <- sim1.res[, c("logFC", "Neighbourhood", "SpatialFDR")]
neighbor.df <- do.call(cbind.data.frame, list(neighbor.df, as.data.frame(neighbourhood.umap$layout)))
colnames(neighbor.df) <- c("logFC", "Neighbourhood", "SpatialFDR", "UMAP1", "UMAP2")
neighbor.df$Sig <- as.numeric(neighbor.df$SpatialFDR <= 0.05)
ggplot(neighbor.df, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=neighbor.df[neighbor.df$Sig == 0, ],
             colour='grey80', size=2) +
  geom_point(data=neighbor.df[neighbor.df$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red")

In each neighbourhood, what is the most common condition or block of cells?

neighbour.list <- list()
for(x in seq_along(1:length(vertex.list))){
  x.df <- meta.df[meta.df$Vertex %in% vertex.list[[x]], ]
  x.rep <- names(table(x.df$Replicate))[which(table(x.df$Replicate) == max(table(x.df$Replicate)))]
  if(length(x.rep) > 1){
    x.rep <- sample(size=1, x.rep)
  }
  x.block <- names(table(x.df$Block))[which(table(x.df$Block) == max(table(x.df$Block)))]
    if(length(x.block) > 1){
    x.block <- sample(size=1, x.block)
  }
  x.condition <- names(table(x.df$Condition))[which(table(x.df$Condition) == max(table(x.df$Condition)))]
    if(length(x.condition) > 1){
    x.condition <- sample(size=1, x.condition)
  }
  
  neighbour.list[[x]] <- data.frame("Replicate"=x.rep, "Block"=x.block, "Condition"=x.condition, "Neighbourhood"=x)
}
neighbour.meta <- do.call(rbind.data.frame, neighbour.list)
neighbour.merge <- merge(neighbor.df, neighbour.meta, by='Neighbourhood')
neighbour.merge$Block <- ordered(neighbour.merge$Block,
                                 levels=c("B1", "B2", "B3"))
neighbour.merge$Diff <- sign(neighbour.merge$logFC)
neighbour.merge$Diff[neighbour.merge$Sig == 0] <- 0
ggplot(neighbour.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=neighbour.merge[, c("UMAP1", "UMAP2")],
             colour='grey80', size=1) +
  geom_point(data=neighbour.merge[neighbour.merge$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red") +
  facet_wrap(~Block)

This doesn’t make sense - Block 3 shouldn’t have any DA neighbourhoods. Is this a compositional effect we’re seeing here? It’s strange that a random fluctuation would cause this - it must be incredibly sensitive.

table(neighbour.merge$Block, neighbour.merge$Diff)
    
     -1  0  1
  B1 66  0  0
  B2  0  0 56
  B3  3 57  0

Is this due to some weird global scaling differences or is this a compositional effect? In addition to the high false-positive rate, there are also sign differences where they should be concordant within a block of data points. For each neighbourhood, how much does it overlap with the over blocks? Intuitively I would have expected 0 as they are well separated, however, this might be a cause for all these false DA neighbourhoods.

NB: This was due to different vertices being sampled, so the results weren’t concordant - set the same seed Mike!!!!

The false-positive rate is a little high here, but at least the signs are the correct way around.

plot(x=sim1.res$logFC, y=-log10(sim1.res$SpatialFDR), xlab="log FC", ylab="Spatial FDR",
     col=ifelse(sim1.res$Sig == 1, "red", "black"))

For each neighbourhood, I need to count the number of points in each block, as well as condition and replicate.

all.samps <- unique(paste(meta.df$Block, meta.df$Condition, meta.df$Replicate, sep="_"))
meta.df$All.Sample <- paste(meta.df$Block, meta.df$Condition, meta.df$Replicate, sep="_")
all.count.matrix <- matrix(0L, ncol=length(all.samps), nrow=length(vertex.list))
colnames(all.count.matrix) <- all.samps
  
for(x in seq_along(1:length(vertex.list))){
  v.x <- vertex.list[[x]]
  for(i in seq_along(1:length(all.samps))){
    i.s <- all.samps[i]
    i.s.vertices <- intersect(v.x, meta.df[meta.df$All.Sample == i.s, ]$Vertex)
    all.count.matrix[x, i] <- length(i.s.vertices)
  }
}
all.count.melt <- melt(all.count.matrix)
all.count.melt$Var2 <- as.character(all.count.melt$Var2)
all.count.melt$Block <- unlist(lapply(strsplit(all.count.melt$Var2, split="_", fixed=TRUE),
                                      FUN=function(XP) paste0(XP[1])))
all.count.melt$Condition <- unlist(lapply(strsplit(all.count.melt$Var2, split="_", fixed=TRUE),
                                      FUN=function(XP) paste0(XP[2])))
all.count.melt$Replicate <- unlist(lapply(strsplit(all.count.melt$Var2, split="_", fixed=TRUE),
                                      FUN=function(XP) paste0(XP[3])))
ggplot(all.count.melt, aes(x=Block, y=value, fill=Condition)) +
  geom_boxplot() +
  theme_clean() +
  facet_wrap(~Var1, scales="free_y")

Each panel is a neighbourhood, the numbers the counts of data points in each that com from either condition, and in the block of points. These are now concordant with the DA results, except neighbourhoods 7 and 26 - which look like a combination of sampling variance, which then creates a compositional effect. Either a filter on low-count neighbourhoods or on the log fold changes would ameliorate this.

NB: Emma suggested using the within neighbourhood distances instead of connectivity as a measure of density. This would require making our own KNN-graph building function that retains the distances between all pairs of vertices. In truth, this could just be added as the edge weights to the KNN-graph, which are ultimately ignored for the purpose of graph building, i.e. all edges = 1.

Using distance between vertices for spatial FDR correction.

We have the PC coordinates that were used in the original KNN graph building, so strictly we just need this with the neighbourhood list to calculate the Euclidean distances. For the neighbourhood, do we take the average off-diagonal distance as the density?

neighbour.dist.list <- list()
for(x in seq_along(1:length(vertex.list))){
  x.verts <- vertex.list[[x]]
  x.pcs <- sim1.pca$x[x.verts, c(1:30)]
  x.euclid <- as.matrix(dist(x.pcs))
  x.distdens <- 1/mean(x.euclid[lower.tri(x.euclid, diag=FALSE)])
  neighbour.dist.list[[x]] <- x.distdens
}
hist(unlist(neighbour.dist.list), 100, xlab="1/mean Euclidean distance")

This looks a little like the 3 blocks of points… Let’s try this for weighting the spatial FDR.

graph_spatialFDR <- function(neighborhoods, graph, pvalues, connectivity='vertex', pca=NULL){
  # input a set of neighborhoods as a list of graph vertices
  # the input graph and the unadjusted GLM p-values
  #' neighborhoods: list of vertices and their respective neighborhoods
  #' graph: input kNN graph
  #' pvalues: a vector of pvalues in the same order as the neighborhood indices
  #' connectivity: character - edge or vertex to calculate neighborhood connectivity or distance to use average Euclidean distance
  #' pca: matrix of PCs to calculate Euclidean distances, only required when connectivity == distance
  # Discarding NA pvalues.
  haspval <- !is.na(pvalues)
  if (!all(haspval)) {
      coords <- coords[haspval, , drop=FALSE]
      pvalues <- pvalues[haspval]
  }
    
  # define the subgraph for each neighborhood then calculate the vertex connectivity for each
  # this latter computation is quite slow - can it be sped up?
  subgraphs <- lapply(1:length(neighborhoods[haspval]),
                         FUN=function(X) induced_subgraph(graph, neighborhoods[haspval][[X]]))
  # now loop over these sub-graphs to calculate the connectivity - this seems a little slow...
  if(connectivity == "vertex"){
    t.connect <- lapply(subgraphs, FUN=function(EG) vertex_connectivity(EG))
  } else if(connectivity == "edge"){
    t.connect <- lapply(subgraphs, FUN=function(EG) edge_connectivity(EG))
  } else if(connectivity == "distance"){
    if(!is.null(pca)){
      t.connect <- lapply(1:length(neighborhoods[haspval]),
                        FUN=function(PG) {
                          x.pcs <- pca[neighborhoods[haspval][[PG]], ]
                          x.euclid <- as.matrix(dist(x.pcs))
                          x.distdens <- 1/mean(x.euclid[lower.tri(x.euclid, diag=FALSE)])
                        return(x.distdens)})
    } else{
      errorCondition("A matrix of PCs is required to calculate distances")  
    }
  }else{
    errorCondition("connectivity option not recognised - must be either edge, vertex or distance")
  }
  
  # use 1/connectivity as the weighting for the weighted BH adjustment from Cydar
  w <- 1/unlist(t.connect)
  w[is.infinite(w)] <- 0
  
  # Computing a density-weighted q-value.
  o <- order(pvalues)
  pvalues <- pvalues[o]
  w <- w[o]
  adjp <- numeric(length(o))
  adjp[o] <- rev(cummin(rev(sum(w)*pvalues/cumsum(w))))
  adjp <- pmin(adjp, 1)
  if (!all(haspval)) {
    refp <- rep(NA_real_, length(haspval))
    refp[haspval] <- adjp
    adjp <- refp
    }
  return(adjp)
}
start.time <- Sys.time()
sim1.spatialfdr <- graph_spatialFDR(neighborhoods=vertex.list, graph=sim1.knn, connectivity="distance",
                                    pca=sim1.pca$x[, c(1:30)],
                                    pvalues=sim1.res[order(sim1.res$Neighbourhood), ]$PValue)
end.time <- Sys.time()
distance.time <- end.time - start.time
sim1.res$SpatialFDR.Dist[order(sim1.res$Neighbourhood)] <- sim1.spatialfdr
qvals <- sim1.spatialfdr
is.sig <- qvals <= 0.01
summary(is.sig)
   Mode   FALSE    TRUE 
logical      62     120 

How does this compare with using connectivity for the spatial FDR correction?

sink(file="/dev/null")
pdf("~/Dropbox/Milo/simulations/Connectivity_vs_distance_SpatialFDR.pdf",
    heigh=3.15, width=4.25, useDingbats=FALSE)
plot(-log10(sim1.res$SpatialFDR.Dist), -log10(sim1.res$SpatialFDR), xlab="-log10 distance Spatial FDR",
     ylab="-log10 connectivity Spatial FDR")
dev.off()
sink(file=NULL)
plot(-log10(sim1.res$SpatialFDR.Dist), -log10(sim1.res$SpatialFDR), xlab="-log10 distance Spatial FDR",
     ylab="-log10 connectivity Spatial FDR")

Aside from the small number of false-positive samples, this works fairly well on these simulated data. Can we also get similar results with a real-world data? For this ’ll test the 1 and 52 week old TEC from our Ageing thymus paper, as these have the biggest differences between them.

Ageing TEC

# read in normalised data, subset to 1 and 52 week old TEC, do a PCA and construct a kNN graph.
tec.meta <- read.table("~/Dropbox/AgeingExperiment/Frozen/ThymusMarker_tSNE_PCA_meta.tsv",
                       sep="\t", header=TRUE, stringsAsFactors=FALSE)
# exclude technical artifact cluster
tec.meta <- tec.meta[!tec.meta$TFIDF.Cluster %in% c(4), ]
tec.sub.meta <- tec.meta[tec.meta$Age %in% c("1wk", "52wk"), ]
# add the label annotation
tec.sub.meta$Cluster <- "Unknown"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "2"] <- "Intertypical TEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "9"] <- "Perinatal cTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "3"] <- "Mature cTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "7"] <- "Mature mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "1"] <- "Post-Aire mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "5"] <- "Tuft-like mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "6"] <- "Proliferating TEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "8"] <- "nTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "10"] <- "sTEC"
inter.cols <- c("#9970ab", "#35978f", "#B0cdc1", "#762a83", "#01665e", "#e7d4e8", "#dfc27d", "#8c510a" ,"#bf812d")
names(inter.cols) <- c("Post-Aire mTEC", 'Intertypical TEC', 'Mature cTEC', 'Tuft-like mTEC', 
                       'Proliferating TEC', 'Mature mTEC', 'nTEC', 'Perinatal cTEC', 'sTEC')
tec.gex <- read.table("~/Dropbox/AgeingExperiment/Frozen/ThymusQC_SFnorm.tsv.gz",
                      sep="\t", header=TRUE, stringsAsFactors=FALSE)
tec.sub.gex <- tec.gex[, colnames(tec.gex) %in% tec.sub.meta$Sample]
tec.hvgs <- read.table("~/Dropbox/AgeingExperiment/Frozen/Thymus_HVG.tsv",
                       sep="\t", header=TRUE, stringsAsFactors=FALSE)

I’ll build a kNN-graph from the first 30 PCs previously computed on all TEC.

set.seed(42)
tec.knn <- buildKNNGraph(x=as.matrix(tec.sub.meta[, paste0("PC", 1:30)]), k=21, d=NA, transposed=TRUE)
tec.fr.layout <- layout_with_fr(tec.knn)
plot(tec.knn, layout=tec.fr.layout, vertex.frame.color='skyblue', vertex.color='skyblue', vertex.label.color='black', 
     vertex.label.family='Helvetica', edge.color='grey60', vertex.label.cex=0.9,
     vertex.label.dist=1, edge.arrow.size=0.2)

This is a fairly densely connected network, how does the UMAP look?

set.seed(42)
tec.umap <- umap(as.matrix(tec.sub.meta[, paste0("PC", 1:30)]),
                 n_components=2,
                 n_neighbors=21, metric='euclidean',
                 init='random', min_dist=0.2)
tec.umap.df <- as.data.frame(tec.umap$layout)
colnames(tec.umap.df) <- c("UMAP1", "UMAP2")
tec.umap.df$Sample <- tec.sub.meta$Sample
tec.umap.merge <- merge(tec.umap.df, tec.sub.meta, by='Sample')
ggplot(tec.umap.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(aes(colour=Cluster)) +
  theme_clean() +
  scale_colour_manual(values=inter.cols) +
  facet_wrap(~Age) +
  guides(colour=guide_legend(override.aes=list(size=3)),
         shape=guide_legend(override.aes=list(size=3)))

# randomly select vertices in the graph
n.hood <- 0.05
tec.random.vertices <- sample(V(tec.knn), size=floor(n.hood*length(V(tec.knn))))
# loop over random vertices and count the number of cells in each
tec.vertex.list <- sapply(1:length(tec.random.vertices), FUN=function(X) neighbors(tec.knn, v=tec.random.vertices[X]))
hist(unlist(lapply(tec.vertex.list, length)), 100, main="Histogram of neighbors", xlab="Neighbourhood size")

This is the histogram of TEC neighbourhood sizes.

tec.umap.merge$ExpSamp <- paste(tec.umap.merge$Age, tec.umap.merge$SortDay, sep="_")
tec.umap.merge$Vertex <- c(1:nrow(tec.umap.merge))
tec.counts <- quant_neighbourhood(graph=tec.knn, meta=tec.umap.merge, sample.column='ExpSamp', sample.vertices=n.hood)
tec.reps <- unlist(lapply(strsplit(unique(tec.umap.merge$ExpSamp), split="_"), FUN=function(X) paste0(X[2])))
tec.cond <- unlist(lapply(strsplit(unique(tec.umap.merge$ExpSamp), split="_"), FUN=function(X) paste0(X[1])))
tec.sample.meta <- data.frame("Condition"=tec.cond,
                              "Replicate"=tec.reps)
tec.sample.meta$Sample <- paste(tec.sample.meta$Condition, tec.sample.meta$Replicate, sep="_")
rownames(tec.sample.meta) <- tec.sample.meta$Sample
tec.model <- model.matrix(~ Condition, data=tec.sample.meta)
head(tec.model)
       (Intercept) Condition52wk
1wk_4            1             0
52wk_5           1             1
1wk_5            1             0
1wk_2            1             0
1wk_1            1             0
1wk_3            1             0
tec.dge <- DGEList(tec.counts[, rownames(tec.model)], lib.size=log(colSums(tec.counts[, rownames(tec.model)])))
tec.dge <- estimateDisp(tec.dge, tec.model)
tec.fit <- glmQLFit(tec.dge, tec.model, robust=TRUE)
# tec.contrast <- makeContrasts(ConditionA - ConditionB, levels=tec.model)
# tec.res <- glmQLFTest(tec.fit, contrast=tec.contrast)
tec.res <- as.data.frame(topTags(glmQLFTest(tec.fit, coef=2), sort.by='none', n=Inf))
tec.res$Sig <- as.factor(as.numeric(tec.res$FDR <= 0.01))
tec.res$Neighbourhood <- as.numeric(rownames(tec.res))
# control the spatial FDR
qvals <- tec.res$PValue
is.sig <- qvals <= 0.01
summary(is.sig)
   Mode   FALSE    TRUE 
logical      27      18 

There are 18 DA neighbourhoods - I expect these should reflect the Perinatal, Intertypical, Proliferating and sTEC. I’ll use the distance-based approach to correct for the spatial FDR.

tec.spatialfdr <- graph_spatialFDR(neighborhoods=tec.vertex.list, graph=tec.knn, connectivity="distance",
                                   pca=as.matrix(tec.sub.meta[, paste0("PC", 1:30)]),
                                   pvalues=tec.res[order(tec.res$Neighbourhood), ]$PValue)
tec.res$SpatialFDR[order(tec.res$Neighbourhood)] <- tec.spatialfdr
qvals <- tec.spatialfdr
is.sig <- qvals <= 0.01
summary(is.sig)
   Mode   FALSE    TRUE 
logical      30      15 

Interesting, 3 neighbourhoods are no longer statistically significant after the spatial FDR correction - hopefully these are genuinely false-positives.

In each neighbourhood, what is the most common condition or block of cells?

tec.neighbour.exprs <- neighborhood_expression(tec.vertex.list, tec.sub.gex)

Embed these hyperspheres with a PCA and UMAP.

tec.neighbour.pca <- prcomp((t(tec.neighbour.exprs[tec.hvgs$HVG, ])))
pairs(tec.neighbour.pca$x[, c(1:5)])

set.seed(42)
neighbourhood.umap <- umap(tec.neighbour.pca$x[, c(1:30)],
                           n_components=2,
                           n_neighbors=21, metric='euclidean',
                           init='random', min_dist=0.1)

We can overlay the DA testing on these neighbourhoods.

tec.neighbor.df <- tec.res[, c("logFC", "Neighbourhood", "SpatialFDR")]
tec.neighbor.df <- do.call(cbind.data.frame, list(tec.neighbor.df, as.data.frame(neighbourhood.umap$layout)))
colnames(tec.neighbor.df) <- c("logFC", "Neighbourhood", "SpatialFDR", "UMAP1", "UMAP2")
tec.neighbor.df$Sig <- as.numeric(tec.neighbor.df$SpatialFDR <= 0.05)
ggplot(tec.neighbor.df, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=tec.neighbor.df[tec.neighbor.df$Sig == 0, ],
             colour='grey80', size=2) +
  geom_point(data=tec.neighbor.df[tec.neighbor.df$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red")

Some are up and some are down. Which TEC subtypes do they largely correspond with?

tec.neighbour.list <- list()
for(x in seq_along(1:length(tec.vertex.list))){
  x.df <- tec.umap.merge[tec.umap.merge$Vertex %in% tec.vertex.list[[x]], ]
  x.rep <- names(table(x.df$SortDay))[which(table(x.df$SortDay) == max(table(x.df$SortDay)))]
  if(length(x.rep) > 1){
    x.rep <- sample(size=1, x.rep)
  }
  x.block <- names(table(x.df$Cluster))[which(table(x.df$Cluster) == max(table(x.df$Cluster)))]
    if(length(x.block) > 1){
    x.block <- sample(size=1, x.block)
  }
  x.condition <- names(table(x.df$Age))[which(table(x.df$Age) == max(table(x.df$Age)))]
    if(length(x.condition) > 1){
    x.condition <- sample(size=1, x.condition)
  }
  
  tec.neighbour.list[[x]] <- data.frame("Replicate"=x.rep, "Cluster"=x.block, "Condition"=x.condition, "Neighbourhood"=x)
}
tec.neighbour.meta <- do.call(rbind.data.frame, tec.neighbour.list)
tec.neighbour.merge <- merge(tec.neighbor.df, tec.neighbour.meta, by='Neighbourhood')
tec.neighbour.merge$Diff <- sign(tec.neighbour.merge$logFC)
tec.neighbour.merge$Diff[tec.neighbour.merge$Sig == 0] <- 0
ggplot(tec.neighbour.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=tec.neighbour.merge[, c("UMAP1", "UMAP2")],
             colour='grey80', size=1) +
  geom_point(data=tec.neighbour.merge[tec.neighbour.merge$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red") +
  facet_wrap(~Cluster)

There is a subset of the Proliferating TEC that are down, good, as are the Perinatal cTEC. Likewise, most of the Intertypical TEC are up as well, but some are also down. I don’t know if this is because of a compositional effect or because these are the ones that would be differentiating into mTEC via the Proliferating TEC compartment.

table(tec.neighbour.merge$Cluster, tec.neighbour.merge$Diff)
                   
                    -1  0  1
  Mature mTEC        0 16  0
  Intertypical TEC   2  6 10
  Perinatal cTEC     4  0  0
  Mature cTEC        0  1  3
  Post-Aire mTEC     0  2  0
  Proliferating TEC  1  0  0

I would say that these results make a lot of sense. Can I now extend this to include all time points and fit age as a linear ordinal variable?

Extended TEC DA testing.

# exclude technical artifact cluster
tec.meta <- tec.meta[!tec.meta$TFIDF.Cluster %in% c(4), ]
tec.sub.meta <- tec.meta
tec.sub.meta$AgeFactor <- ordered(tec.sub.meta$Age,
                                  levels=c("1wk", "4wk", "16wk", "32wk", "52wk"))
# add the label annotation
tec.sub.meta$Cluster <- "Unknown"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "2"] <- "Intertypical TEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "9"] <- "Perinatal cTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "3"] <- "Mature cTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "7"] <- "Mature mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "1"] <- "Post-Aire mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "5"] <- "Tuft-like mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "6"] <- "Proliferating TEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "8"] <- "nTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "10"] <- "sTEC"
inter.cols <- c("#9970ab", "#35978f", "#B0cdc1", "#762a83", "#01665e", "#e7d4e8", "#dfc27d", "#8c510a" ,"#bf812d")
names(inter.cols) <- c("Post-Aire mTEC", 'Intertypical TEC', 'Mature cTEC', 'Tuft-like mTEC', 
                       'Proliferating TEC', 'Mature mTEC', 'nTEC', 'Perinatal cTEC', 'sTEC')
tec.sub.gex <- tec.gex[, colnames(tec.gex) %in% tec.sub.meta$Sample]

I’ll build a kNN-graph from the first 30 PCs previously computed on all TEC.

set.seed(42)
tec.knn <- buildKNNGraph(x=as.matrix(tec.sub.meta[, paste0("PC", 1:30)]), k=21, d=NA, transposed=TRUE)
tec.fr.layout <- layout_with_fr(tec.knn)
plot(tec.knn, layout=tec.fr.layout, vertex.frame.color='skyblue', vertex.color='skyblue', vertex.label.color='black', 
     vertex.label.family='Helvetica', edge.color='grey60', vertex.label.cex=0.9,
     vertex.label.dist=1, edge.arrow.size=0.2)

This is a fairly densely connected network, how does the UMAP look?

set.seed(42)
tec.umap <- umap(as.matrix(tec.sub.meta[, paste0("PC", 1:30)]),
                 n_components=2,
                 n_neighbors=21, metric='euclidean',
                 init='random', min_dist=0.2)
tec.umap.df <- as.data.frame(tec.umap$layout)
colnames(tec.umap.df) <- c("UMAP1", "UMAP2")
tec.umap.df$Sample <- tec.sub.meta$Sample
tec.umap.merge <- merge(tec.umap.df, tec.sub.meta, by='Sample')
ggplot(tec.umap.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(aes(colour=Cluster)) +
  theme_clean() +
  scale_colour_manual(values=inter.cols) +
  facet_wrap(~AgeFactor) +
  guides(colour=guide_legend(override.aes=list(size=3)),
         shape=guide_legend(override.aes=list(size=3)))

# randomly select vertices in the graph
n.hood <- 0.05
tec.random.vertices <- sample(V(tec.knn), size=floor(n.hood*length(V(tec.knn))))
# loop over random vertices and count the number of cells in each
tec.vertex.list <- sapply(1:length(tec.random.vertices), FUN=function(X) neighbors(tec.knn, v=tec.random.vertices[X]))
hist(unlist(lapply(tec.vertex.list, length)), 100, main="Histogram of neighbors", xlab="Neighbourhood size")

This is the histogram of TEC neighbourhood sizes.

tec.umap.merge$ExpSamp <- paste(tec.umap.merge$Age, tec.umap.merge$SortDay, sep="_")
tec.umap.merge$Vertex <- c(1:nrow(tec.umap.merge))
tec.counts <- quant_neighbourhood(graph=tec.knn, meta=tec.umap.merge, sample.column='ExpSamp', sample.vertices=n.hood)
tec.reps <- unlist(lapply(strsplit(unique(tec.umap.merge$ExpSamp), split="_"), FUN=function(X) paste0(X[2])))
tec.cond <- unlist(lapply(strsplit(unique(tec.umap.merge$ExpSamp), split="_"), FUN=function(X) paste0(X[1])))
tec.sample.meta <- data.frame("Condition"=tec.cond,
                              "Replicate"=tec.reps)
tec.sample.meta$Sample <- paste(tec.sample.meta$Condition, tec.sample.meta$Replicate, sep="_")
rownames(tec.sample.meta) <- tec.sample.meta$Sample
tec.sample.meta$Condition <- ordered(tec.sample.meta$Condition,
                                     levels=c("1wk", "4wk", "16wk", "32wk", "52wk"))
tec.model <- model.matrix(~ Condition, data=tec.sample.meta)
head(tec.model)
       (Intercept) Condition.L Condition.Q Condition.C Condition^4
1wk_4            1  -0.6324555   0.5345225  -0.3162278   0.1195229
4wk_3            1  -0.3162278  -0.2672612   0.6324555  -0.4780914
32wk_2           1   0.3162278  -0.2672612  -0.6324555  -0.4780914
32wk_5           1   0.3162278  -0.2672612  -0.6324555  -0.4780914
52wk_5           1   0.6324555   0.5345225   0.3162278   0.1195229
4wk_2            1  -0.3162278  -0.2672612   0.6324555  -0.4780914
tec.dge <- DGEList(tec.counts[, rownames(tec.model)], lib.size=log(colSums(tec.counts[, rownames(tec.model)])))
tec.dge <- estimateDisp(tec.dge, tec.model)
tec.fit <- glmQLFit(tec.dge, tec.model, robust=TRUE)
# tec.contrast <- makeContrasts(ConditionA - ConditionB, levels=tec.model)
# tec.res <- glmQLFTest(tec.fit, contrast=tec.contrast)
tec.res <- as.data.frame(topTags(glmQLFTest(tec.fit, coef=2), sort.by='none', n=Inf))
tec.res$Sig <- as.factor(as.numeric(tec.res$FDR <= 0.01))
tec.res$Neighbourhood <- as.numeric(rownames(tec.res))
# control the spatial FDR
qvals <- tec.res$PValue
is.sig <- qvals <= 0.01
summary(is.sig)
   Mode   FALSE    TRUE 
logical      70      46 

There are 46 DA neighbourhoods - I expect these should reflect the Perinatal, Intertypical, Proliferating and sTEC. I’ll use the distance-based approach to correct for the spatial FDR.

tec.spatialfdr <- graph_spatialFDR(neighborhoods=tec.vertex.list, graph=tec.knn, connectivity="distance",
                                   pca=as.matrix(tec.sub.meta[, paste0("PC", 1:30)]),
                                   pvalues=tec.res[order(tec.res$Neighbourhood), ]$PValue)
tec.res$SpatialFDR[order(tec.res$Neighbourhood)] <- tec.spatialfdr
qvals <- tec.spatialfdr
is.sig <- qvals <= 0.01
summary(is.sig)
   Mode   FALSE    TRUE 
logical      82      34 

Interesting, 12 neighbourhoods are no longer statistically significant after the spatial FDR correction - hopefully these are genuinely false-positives.

In each neighbourhood, what is the most common condition or block of cells?

tec.neighbour.exprs <- neighborhood_expression(tec.vertex.list, tec.sub.gex)

Embed these hyperspheres with a PCA and UMAP.

tec.neighbour.pca <- prcomp((t(tec.neighbour.exprs[tec.hvgs$HVG, ])))
pairs(tec.neighbour.pca$x[, c(1:5)])

set.seed(42)
neighbourhood.umap <- umap(tec.neighbour.pca$x[, c(1:30)],
                           n_components=2,
                           n_neighbors=21, metric='euclidean',
                           init='random', min_dist=0.1)

We can overlay the DA testing on these neighbourhoods.

tec.neighbor.df <- tec.res[, c("logFC", "Neighbourhood", "SpatialFDR")]
tec.neighbor.df <- do.call(cbind.data.frame, list(tec.neighbor.df, as.data.frame(neighbourhood.umap$layout)))
colnames(tec.neighbor.df) <- c("logFC", "Neighbourhood", "SpatialFDR", "UMAP1", "UMAP2")
tec.neighbor.df$Sig <- as.numeric(tec.neighbor.df$SpatialFDR <= 0.05)
ggplot(tec.neighbor.df, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=tec.neighbor.df[tec.neighbor.df$Sig == 0, ],
             colour='grey80', size=2) +
  geom_point(data=tec.neighbor.df[tec.neighbor.df$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red")

Some are up and some are down. Which TEC subtypes do they largely correspond with? That big streak of lower abundance neighbourhoods should be the differentiation trajectory from Intertypical to Mature mTEC.

tec.neighbour.list <- list()
for(x in seq_along(1:length(tec.vertex.list))){
  x.df <- tec.umap.merge[tec.umap.merge$Vertex %in% tec.vertex.list[[x]], ]
  x.rep <- names(table(x.df$SortDay))[which(table(x.df$SortDay) == max(table(x.df$SortDay)))]
  if(length(x.rep) > 1){
    x.rep <- sample(size=1, x.rep)
  }
  x.block <- names(table(x.df$Cluster))[which(table(x.df$Cluster) == max(table(x.df$Cluster)))]
    if(length(x.block) > 1){
    x.block <- sample(size=1, x.block)
  }
  x.condition <- names(table(x.df$Age))[which(table(x.df$Age) == max(table(x.df$Age)))]
    if(length(x.condition) > 1){
    x.condition <- sample(size=1, x.condition)
  }
  
  tec.neighbour.list[[x]] <- data.frame("Replicate"=x.rep, "Cluster"=x.block, "Condition"=x.condition, "Neighbourhood"=x)
}
tec.neighbour.meta <- do.call(rbind.data.frame, tec.neighbour.list)
tec.neighbour.merge <- merge(tec.neighbor.df, tec.neighbour.meta, by='Neighbourhood')
tec.neighbour.merge$Diff <- sign(tec.neighbour.merge$logFC)
tec.neighbour.merge$Diff[tec.neighbour.merge$Sig == 0] <- 0
ggplot(tec.neighbour.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=tec.neighbour.merge[, c("UMAP1", "UMAP2")],
             colour='grey80', size=1) +
  geom_point(data=tec.neighbour.merge[tec.neighbour.merge$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red") +
  facet_wrap(~Cluster)

This very nicely recapitulates the DA testing using clusters, and it pinpoints the loss of medulla-biased Intertypical TEC which we only really observed in our larger experiments. This is working beyond my wildest dreams!!

table(tec.neighbour.merge$Cluster, tec.neighbour.merge$Diff)
                   
                    -1  0  1
  Mature mTEC        4 32  2
  Proliferating TEC  7  4  0
  Intertypical TEC   8 12 18
  Perinatal cTEC     5  0  0
  sTEC               0  0  1
  Post-Aire mTEC     0  7  0
  Mature cTEC        0  6  5
  nTEC               0  2  0
  Tuft-like mTEC     0  3  0

I would say that these results make a lot of sense. I’ll extend it to include the quadratic testing which should pick up the inverse-parabolic profile of the Post-Aire mTEC population.

quad.tec.res <- as.data.frame(topTags(glmQLFTest(tec.fit, coef=3), sort.by='none', n=Inf))
quad.tec.res$Sig <- as.factor(as.numeric(quad.tec.res$FDR <= 0.01))
quad.tec.res$Neighbourhood <- as.numeric(rownames(quad.tec.res))
# control the spatial FDR
qvals <- quad.tec.res$PValue
is.sig <- qvals <= 0.01
summary(is.sig)
   Mode   FALSE    TRUE 
logical     110       6 

There are 6 DA neighbourhoods from the quadratic model - I expect these should reflect the Post-Aire mTEC.

quad.tec.spatialfdr <- graph_spatialFDR(neighborhoods=tec.vertex.list, graph=tec.knn, connectivity="distance",
                                   pca=as.matrix(tec.sub.meta[, paste0("PC", 1:30)]),
                                   pvalues=quad.tec.res[order(quad.tec.res$Neighbourhood), ]$PValue)
quad.tec.res$SpatialFDR[order(quad.tec.res$Neighbourhood)] <- quad.tec.spatialfdr
qvals <- quad.tec.spatialfdr
is.sig <- qvals <= 0.05
summary(is.sig)
   Mode   FALSE 
logical     116 

Dang, clearly the small group of Post-Aire mTEC means this isn’t sufficiently sensitive after multiple-testing correction.

ggplot(tec.neighbour.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=tec.neighbour.merge[, c("UMAP1", "UMAP2")],
             colour='grey80', size=1) +
  geom_point(data=tec.neighbour.merge,
             aes(colour=Cluster), size=4) +
  theme_clean() +
  scale_colour_manual(values=inter.cols) +
  facet_wrap(~Cluster)

Hmm, the Post-Aire mTEC don’t form a single neighbourhood on their own, nor do the nTEC or Tuft-like mTEC. There is definitely a limit to the resolution. Would a smaller \(k\) resolve this?

Ageing TEC with k=11

tec.sub.meta <- tec.meta[tec.meta$Age %in% c("1wk", "52wk"), ]
# add the label annotation
tec.sub.meta$Cluster <- "Unknown"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "2"] <- "Intertypical TEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "9"] <- "Perinatal cTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "3"] <- "Mature cTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "7"] <- "Mature mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "1"] <- "Post-Aire mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "5"] <- "Tuft-like mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "6"] <- "Proliferating TEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "8"] <- "nTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "10"] <- "sTEC"
inter.cols <- c("#9970ab", "#35978f", "#B0cdc1", "#762a83", "#01665e", "#e7d4e8", "#dfc27d", "#8c510a" ,"#bf812d")
names(inter.cols) <- c("Post-Aire mTEC", 'Intertypical TEC', 'Mature cTEC', 'Tuft-like mTEC', 
                       'Proliferating TEC', 'Mature mTEC', 'nTEC', 'Perinatal cTEC', 'sTEC')

I’ll build a kNN-graph from the first 30 PCs previously computed on all TEC, but k=11 this time.

set.seed(42)
tec.knn <- buildKNNGraph(x=as.matrix(tec.sub.meta[, paste0("PC", 1:30)]), k=11, d=NA, transposed=TRUE)
tec.fr.layout <- layout_with_fr(tec.knn)
plot(tec.knn, layout=tec.fr.layout, vertex.frame.color='skyblue', vertex.color='skyblue', vertex.label.color='black', 
     vertex.label.family='Helvetica', edge.color='grey60', vertex.label.cex=0.9,
     vertex.label.dist=1, edge.arrow.size=0.2)

This is a fairly densely connected network still, even with k=11, how does the UMAP look?

set.seed(42)
tec.umap <- umap(as.matrix(tec.sub.meta[, paste0("PC", 1:30)]),
                 n_components=2,
                 n_neighbors=11, metric='euclidean',
                 init='random', min_dist=0.2)
tec.umap.df <- as.data.frame(tec.umap$layout)
colnames(tec.umap.df) <- c("UMAP1", "UMAP2")
tec.umap.df$Sample <- tec.sub.meta$Sample
tec.umap.merge <- merge(tec.umap.df, tec.sub.meta, by='Sample')
ggplot(tec.umap.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(aes(colour=Cluster)) +
  theme_clean() +
  scale_colour_manual(values=inter.cols) +
  facet_wrap(~Age) +
  guides(colour=guide_legend(override.aes=list(size=3)),
         shape=guide_legend(override.aes=list(size=3)))

For a smaller k, does there need to be a higher density of sampling, i.e. more neighbourhoods? I’ll set it to 10% here instead.

# randomly select vertices in the graph
n.hood <- 0.10
tec.random.vertices <- sample(V(tec.knn), size=floor(n.hood*length(V(tec.knn))))
# loop over random vertices and count the number of cells in each
tec.vertex.list <- sapply(1:length(tec.random.vertices), FUN=function(X) neighbors(tec.knn, v=tec.random.vertices[X]))
hist(unlist(lapply(tec.vertex.list, length)), 100, main="Histogram of neighbors", xlab="Neighbourhood size")

This is the histogram of TEC neighbourhood sizes.

tec.umap.merge$ExpSamp <- paste(tec.umap.merge$Age, tec.umap.merge$SortDay, sep="_")
tec.umap.merge$Vertex <- c(1:nrow(tec.umap.merge))
tec.counts <- quant_neighbourhood(graph=tec.knn, meta=tec.umap.merge, sample.column='ExpSamp', sample.vertices=n.hood)
tec.reps <- unlist(lapply(strsplit(unique(tec.umap.merge$ExpSamp), split="_"), FUN=function(X) paste0(X[2])))
tec.cond <- unlist(lapply(strsplit(unique(tec.umap.merge$ExpSamp), split="_"), FUN=function(X) paste0(X[1])))
tec.sample.meta <- data.frame("Condition"=tec.cond,
                              "Replicate"=tec.reps)
tec.sample.meta$Sample <- paste(tec.sample.meta$Condition, tec.sample.meta$Replicate, sep="_")
rownames(tec.sample.meta) <- tec.sample.meta$Sample
tec.model <- model.matrix(~ Condition, data=tec.sample.meta)
head(tec.model)
       (Intercept) Condition52wk
1wk_4            1             0
52wk_5           1             1
1wk_5            1             0
1wk_2            1             0
1wk_1            1             0
1wk_3            1             0
tec.dge <- DGEList(tec.counts[, rownames(tec.model)], lib.size=log(colSums(tec.counts[, rownames(tec.model)])))
tec.dge <- estimateDisp(tec.dge, tec.model)
tec.fit <- glmQLFit(tec.dge, tec.model, robust=TRUE)
# tec.contrast <- makeContrasts(ConditionA - ConditionB, levels=tec.model)
# tec.res <- glmQLFTest(tec.fit, contrast=tec.contrast)
tec.res <- as.data.frame(topTags(glmQLFTest(tec.fit, coef=2), sort.by='none', n=Inf))
tec.res$Sig <- as.factor(as.numeric(tec.res$FDR <= 0.05))
tec.res$Neighbourhood <- as.numeric(rownames(tec.res))
# control the spatial FDR
qvals <- tec.res$PValue
is.sig <- qvals <= 0.05
summary(is.sig)
   Mode   FALSE    TRUE 
logical      43      48 

I have increased the total number of neighbourhoods defined with k=11, there will almost certainly be a trade-off between sensitivity and power w.r.t. the extra multiple-testing burden, as well as the higher sampling variance as each neighbourhood will contain fewer cells <- this will be something we need to optimise in some way.

tec.spatialfdr <- graph_spatialFDR(neighborhoods=tec.vertex.list, graph=tec.knn, connectivity="distance",
                                   pca=as.matrix(tec.sub.meta[, paste0("PC", 1:30)]),
                                   pvalues=tec.res[order(tec.res$Neighbourhood), ]$PValue)
tec.res$SpatialFDR[order(tec.res$Neighbourhood)] <- tec.spatialfdr
qvals <- tec.spatialfdr
is.sig <- qvals <= 0.05
summary(is.sig)
   Mode   FALSE    TRUE 
logical      54      37 

Interesting, 11 of the neighbourhoods are no long statistically significant after the spatial FDR correction. Clearly there is a trade-off between neighbourhood size, sensitivity and power.

In each neighbourhood, what is the most common condition or block of cells?

tec.neighbour.exprs <- neighborhood_expression(tec.vertex.list, tec.sub.gex)

Embed these hyperspheres with a PCA and UMAP.

tec.neighbour.pca <- prcomp((t(tec.neighbour.exprs[tec.hvgs$HVG, ])))
pairs(tec.neighbour.pca$x[, c(1:5)])

set.seed(42)
neighbourhood.umap <- umap(tec.neighbour.pca$x[, c(1:30)],
                           n_components=2,
                           n_neighbors=11, metric='euclidean',
                           init='random', min_dist=0.1)

We can overlay the DA testing on these neighbourhoods.

tec.neighbor.df <- tec.res[, c("logFC", "Neighbourhood", "SpatialFDR")]
tec.neighbor.df <- do.call(cbind.data.frame, list(tec.neighbor.df, as.data.frame(neighbourhood.umap$layout)))
colnames(tec.neighbor.df) <- c("logFC", "Neighbourhood", "SpatialFDR", "UMAP1", "UMAP2")
tec.neighbor.df$Sig <- as.numeric(tec.neighbor.df$SpatialFDR <= 0.05)
ggplot(tec.neighbor.df, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=tec.neighbor.df[tec.neighbor.df$Sig == 0, ],
             colour='grey80', size=2) +
  geom_point(data=tec.neighbor.df[tec.neighbor.df$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red")

Some are up and some are down. Which TEC subtypes do they largely correspond with?

tec.neighbour.list <- list()
for(x in seq_along(1:length(tec.vertex.list))){
  x.df <- tec.umap.merge[tec.umap.merge$Vertex %in% tec.vertex.list[[x]], ]
  x.rep <- names(table(x.df$SortDay))[which(table(x.df$SortDay) == max(table(x.df$SortDay)))]
  if(length(x.rep) > 1){
    x.rep <- sample(size=1, x.rep)
  }
  x.block <- names(table(x.df$Cluster))[which(table(x.df$Cluster) == max(table(x.df$Cluster)))]
    if(length(x.block) > 1){
    x.block <- sample(size=1, x.block)
  }
  x.condition <- names(table(x.df$Age))[which(table(x.df$Age) == max(table(x.df$Age)))]
    if(length(x.condition) > 1){
    x.condition <- sample(size=1, x.condition)
  }
  
  tec.neighbour.list[[x]] <- data.frame("Replicate"=x.rep, "Cluster"=x.block, "Condition"=x.condition, "Neighbourhood"=x)
}
tec.neighbour.meta <- do.call(rbind.data.frame, tec.neighbour.list)
tec.neighbour.merge <- merge(tec.neighbor.df, tec.neighbour.meta, by='Neighbourhood')
tec.neighbour.merge$Diff <- sign(tec.neighbour.merge$logFC)
tec.neighbour.merge$Diff[tec.neighbour.merge$Sig == 0] <- 0
ggplot(tec.neighbour.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=tec.neighbour.merge[, c("UMAP1", "UMAP2")],
             colour='grey80', size=1) +
  geom_point(data=tec.neighbour.merge[tec.neighbour.merge$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red") +
  facet_wrap(~Cluster)

There is a subset of the Proliferating TEC that are down, good, as are the Perinatal cTEC. Likewise, most of the Intertypical TEC are up as well, but some are also down. I don’t know if this is because of a compositional effect or because these are the ones that would be differentiating into mTEC via the Proliferating TEC compartment.

table(tec.neighbour.merge$Cluster, tec.neighbour.merge$Diff)
                   
                    -1  0  1
  Mature mTEC        0 30  0
  Intertypical TEC   4 14 17
  Perinatal cTEC     8  1  0
  Mature cTEC        1  1  6
  Post-Aire mTEC     0  4  0
  Proliferating TEC  1  3  0
  Tuft-like mTEC     0  1  0

I would say that these results make a lot of sense. Can I now extend this to include all time points and fit age as a linear ordinal variable?

Extended TEC DA testing with k=11

# exclude technical artifact cluster
tec.sub.meta <- tec.meta
tec.sub.meta$AgeFactor <- ordered(tec.sub.meta$Age,
                                  levels=c("1wk", "4wk", "16wk", "32wk", "52wk"))
# add the label annotation
tec.sub.meta$Cluster <- "Unknown"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "2"] <- "Intertypical TEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "9"] <- "Perinatal cTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "3"] <- "Mature cTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "7"] <- "Mature mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "1"] <- "Post-Aire mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "5"] <- "Tuft-like mTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "6"] <- "Proliferating TEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "8"] <- "nTEC"
tec.sub.meta$Cluster[tec.sub.meta$TFIDF.Cluster == "10"] <- "sTEC"
inter.cols <- c("#9970ab", "#35978f", "#B0cdc1", "#762a83", "#01665e", "#e7d4e8", "#dfc27d", "#8c510a" ,"#bf812d")
names(inter.cols) <- c("Post-Aire mTEC", 'Intertypical TEC', 'Mature cTEC', 'Tuft-like mTEC', 
                       'Proliferating TEC', 'Mature mTEC', 'nTEC', 'Perinatal cTEC', 'sTEC')
tec.sub.gex <- tec.gex[, colnames(tec.gex) %in% tec.sub.meta$Sample]

I’ll build a kNN-graph from the first 30 PCs previously computed on all TEC.

set.seed(42)
tec.knn <- buildKNNGraph(x=as.matrix(tec.sub.meta[, paste0("PC", 1:30)]), k=11, d=NA, transposed=TRUE)
tec.fr.layout <- layout_with_fr(tec.knn)
plot(tec.knn, layout=tec.fr.layout, vertex.frame.color='skyblue', vertex.color='skyblue', vertex.label.color='black', 
     vertex.label.family='Helvetica', edge.color='grey60', vertex.label.cex=0.9,
     vertex.label.dist=1, edge.arrow.size=0.2)

This is a fairly densely connected network, how does the UMAP look?

set.seed(42)
tec.umap <- umap(as.matrix(tec.sub.meta[, paste0("PC", 1:30)]),
                 n_components=2,
                 n_neighbors=11, metric='euclidean',
                 init='random', min_dist=0.2)
tec.umap.df <- as.data.frame(tec.umap$layout)
colnames(tec.umap.df) <- c("UMAP1", "UMAP2")
tec.umap.df$Sample <- tec.sub.meta$Sample
tec.umap.merge <- merge(tec.umap.df, tec.sub.meta, by='Sample')
ggplot(tec.umap.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(aes(colour=Cluster)) +
  theme_clean() +
  scale_colour_manual(values=inter.cols) +
  facet_wrap(~AgeFactor) +
  guides(colour=guide_legend(override.aes=list(size=3)),
         shape=guide_legend(override.aes=list(size=3)))

# randomly select vertices in the graph
n.hood <- 0.10
tec.random.vertices <- sample(V(tec.knn), size=floor(n.hood*length(V(tec.knn))))
# loop over random vertices and count the number of cells in each
tec.vertex.list <- sapply(1:length(tec.random.vertices), FUN=function(X) neighbors(tec.knn, v=tec.random.vertices[X]))
hist(unlist(lapply(tec.vertex.list, length)), 100, main="Histogram of neighbors", xlab="Neighbourhood size")

This is the histogram of TEC neighbourhood sizes.

tec.umap.merge$ExpSamp <- paste(tec.umap.merge$Age, tec.umap.merge$SortDay, sep="_")
tec.umap.merge$Vertex <- c(1:nrow(tec.umap.merge))
tec.counts <- quant_neighbourhood(graph=tec.knn, meta=tec.umap.merge, sample.column='ExpSamp', sample.vertices=n.hood)
tec.reps <- unlist(lapply(strsplit(unique(tec.umap.merge$ExpSamp), split="_"), FUN=function(X) paste0(X[2])))
tec.cond <- unlist(lapply(strsplit(unique(tec.umap.merge$ExpSamp), split="_"), FUN=function(X) paste0(X[1])))
tec.sample.meta <- data.frame("Condition"=tec.cond,
                              "Replicate"=tec.reps)
tec.sample.meta$Sample <- paste(tec.sample.meta$Condition, tec.sample.meta$Replicate, sep="_")
rownames(tec.sample.meta) <- tec.sample.meta$Sample
tec.sample.meta$Condition <- ordered(tec.sample.meta$Condition,
                                     levels=c("1wk", "4wk", "16wk", "32wk", "52wk"))
tec.model <- model.matrix(~ Condition, data=tec.sample.meta)
head(tec.model)
       (Intercept) Condition.L Condition.Q Condition.C Condition^4
1wk_4            1  -0.6324555   0.5345225  -0.3162278   0.1195229
4wk_3            1  -0.3162278  -0.2672612   0.6324555  -0.4780914
32wk_2           1   0.3162278  -0.2672612  -0.6324555  -0.4780914
32wk_5           1   0.3162278  -0.2672612  -0.6324555  -0.4780914
52wk_5           1   0.6324555   0.5345225   0.3162278   0.1195229
4wk_2            1  -0.3162278  -0.2672612   0.6324555  -0.4780914
tec.dge <- DGEList(tec.counts[, rownames(tec.model)], lib.size=log(colSums(tec.counts[, rownames(tec.model)])))
tec.dge <- estimateDisp(tec.dge, tec.model)
tec.fit <- glmQLFit(tec.dge, tec.model, robust=TRUE)
# tec.contrast <- makeContrasts(ConditionA - ConditionB, levels=tec.model)
# tec.res <- glmQLFTest(tec.fit, contrast=tec.contrast)
tec.res <- as.data.frame(topTags(glmQLFTest(tec.fit, coef=2), sort.by='none', n=Inf))
tec.res$Sig <- as.factor(as.numeric(tec.res$FDR <= 0.05))
tec.res$Neighbourhood <- as.numeric(rownames(tec.res))
# control the spatial FDR
qvals <- tec.res$PValue
is.sig <- qvals <= 0.05
summary(is.sig)
   Mode   FALSE    TRUE 
logical     110     122 

There are 85 DA neighbourhoods - I expect these should reflect the Perinatal, Intertypical, Proliferating and sTEC. I’ll use the distance-based approach to correct for the spatial FDR.

tec.spatialfdr <- graph_spatialFDR(neighborhoods=tec.vertex.list, graph=tec.knn, connectivity="distance",
                                   pca=as.matrix(tec.sub.meta[, paste0("PC", 1:30)]),
                                   pvalues=tec.res[order(tec.res$Neighbourhood), ]$PValue)
tec.res$SpatialFDR[order(tec.res$Neighbourhood)] <- tec.spatialfdr
qvals <- tec.spatialfdr
is.sig <- qvals <= 0.05
summary(is.sig)
   Mode   FALSE    TRUE 
logical     136      96 

Interesting, there are 26 neighbourhoods no longer statistically significant after the spatial FDR correction - hopefully these are genuinely false-positives.

In each neighbourhood, what is the most common condition or block of cells?

tec.neighbour.exprs <- neighborhood_expression(tec.vertex.list, tec.sub.gex)

Embed these hyperspheres with a PCA and UMAP.

tec.neighbour.pca <- prcomp((t(tec.neighbour.exprs[tec.hvgs$HVG, ])))
pairs(tec.neighbour.pca$x[, c(1:5)])

set.seed(42)
neighbourhood.umap <- umap(tec.neighbour.pca$x[, c(1:30)],
                           n_components=2,
                           n_neighbors=11, metric='euclidean',
                           init='random', min_dist=0.1)

We can overlay the DA testing on these neighbourhoods.

tec.neighbor.df <- tec.res[, c("logFC", "Neighbourhood", "SpatialFDR")]
tec.neighbor.df <- do.call(cbind.data.frame, list(tec.neighbor.df, as.data.frame(neighbourhood.umap$layout)))
colnames(tec.neighbor.df) <- c("logFC", "Neighbourhood", "SpatialFDR", "UMAP1", "UMAP2")
tec.neighbor.df$Sig <- as.numeric(tec.neighbor.df$SpatialFDR <= 0.05)
ggplot(tec.neighbor.df, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=tec.neighbor.df[tec.neighbor.df$Sig == 0, ],
             colour='grey80', size=2) +
  geom_point(data=tec.neighbor.df[tec.neighbor.df$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red")

Some are up and some are down. Which TEC subtypes do they largely correspond with? That big streak of lower abundance neighbourhoods should be the differentiation trajectory from Intertypical to Mature mTEC.

tec.neighbour.list <- list()
for(x in seq_along(1:length(tec.vertex.list))){
  x.df <- tec.umap.merge[tec.umap.merge$Vertex %in% tec.vertex.list[[x]], ]
  x.rep <- names(table(x.df$SortDay))[which(table(x.df$SortDay) == max(table(x.df$SortDay)))]
  if(length(x.rep) > 1){
    x.rep <- sample(size=1, x.rep)
  }
  x.block <- names(table(x.df$Cluster))[which(table(x.df$Cluster) == max(table(x.df$Cluster)))]
    if(length(x.block) > 1){
    x.block <- sample(size=1, x.block)
  }
  x.condition <- names(table(x.df$Age))[which(table(x.df$Age) == max(table(x.df$Age)))]
    if(length(x.condition) > 1){
    x.condition <- sample(size=1, x.condition)
  }
  
  tec.neighbour.list[[x]] <- data.frame("Replicate"=x.rep, "Cluster"=x.block, "Condition"=x.condition, "Neighbourhood"=x)
}
tec.neighbour.meta <- do.call(rbind.data.frame, tec.neighbour.list)
tec.neighbour.merge <- merge(tec.neighbor.df, tec.neighbour.meta, by='Neighbourhood')
tec.neighbour.merge$Diff <- sign(tec.neighbour.merge$logFC)
tec.neighbour.merge$Diff[tec.neighbour.merge$Sig == 0] <- 0
ggplot(tec.neighbour.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=tec.neighbour.merge[, c("UMAP1", "UMAP2")],
             colour='grey80', size=1) +
  geom_point(data=tec.neighbour.merge[tec.neighbour.merge$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red") +
  facet_wrap(~Cluster)

This extended analysis with k=11 also detects the increase in the small sTEC population. There also appears to be more heterogeneity in the Intertypical TEC, and, somewhat ingtriguingly, changes amongst the mature mTEC which were not detected originally.

table(tec.neighbour.merge$Cluster, tec.neighbour.merge$Diff)
                   
                    -1  0  1
  Mature mTEC        5 65  2
  Proliferating TEC 11  6  0
  Intertypical TEC  16 28 43
  Perinatal cTEC     6  3  0
  sTEC               0  0  1
  Post-Aire mTEC     0 14  0
  Mature cTEC        2 14 10
  nTEC               0  3  0
  Tuft-like mTEC     0  3  0

I would say that these results make a lot of sense. I’ll extend it to include the quadratic testing which should pick up the inverse-parabolic profile of the Post-Aire mTEC population.

quad.tec.res <- as.data.frame(topTags(glmQLFTest(tec.fit, coef=3), sort.by='none', n=Inf))
quad.tec.res$Sig <- as.factor(as.numeric(quad.tec.res$FDR <= 0.05))
quad.tec.res$Neighbourhood <- as.numeric(rownames(quad.tec.res))
# control the spatial FDR
qvals <- quad.tec.res$PValue
is.sig <- qvals <= 0.05
summary(is.sig)
   Mode   FALSE    TRUE 
logical     205      27 

There are 27 DA neighbourhoods from the quadratic model - I expect these should reflect the Post-Aire mTEC.

quad.tec.spatialfdr <- graph_spatialFDR(neighborhoods=tec.vertex.list, graph=tec.knn, connectivity="distance",
                                   pca=as.matrix(tec.sub.meta[, paste0("PC", 1:30)]),
                                   pvalues=quad.tec.res[order(quad.tec.res$Neighbourhood), ]$PValue)
quad.tec.res$SpatialFDR[order(quad.tec.res$Neighbourhood)] <- quad.tec.spatialfdr
qvals <- quad.tec.spatialfdr
is.sig <- qvals <= 0.05
summary(is.sig)
   Mode   FALSE 
logical     232 

Dang, clearly still the small group of Post-Aire mTEC means this isn’t sufficiently sensitive after multiple-testing correction.

ggplot(tec.neighbour.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=tec.neighbour.merge[, c("UMAP1", "UMAP2")],
             colour='grey80', size=1) +
  geom_point(data=tec.neighbour.merge,
             aes(colour=Cluster), size=3) +
  theme_clean() +
  scale_colour_manual(values=inter.cols) +
  facet_wrap(~Cluster)

Hmm, the Post-Aire mTEC don’t form a single neighbourhood on their own but rather 3 separate ones. I would say this possibly too granular.

Compositional effects

Firstly, are compositional effects a problem here, and secondly, does the refined sampling scheme handle this? I’ll set up a new simulation that has 2 clusters, only one of which contains differentially abundant neighbourhoods.

set.seed(42)
r.n <- 1000
n.dim <- 50
block1.cells <- 250
# select a set of eigen values for the covariance matrix of each block, say 50 eigenvalues?
block1.eigens <- sapply(1:n.dim, FUN=function(X) rexp(n=1, rate=abs(runif(n=1, min=0, max=50))))
block1.eigens <- block1.eigens[order(block1.eigens)]
block1.p <- qr.Q(qr(matrix(rnorm(block1.cells^2, mean=4, sd=0.01), block1.cells)))
block1.sigma <- crossprod(block1.p, block1.p*block1.eigens)
block1.gex <- abs(rmvnorm(n=r.n, mean=rnorm(n=block1.cells, mean=2, sd=0.01), sigma=block1.sigma))
block3.cells <- 250
# select a set of eigen values for the covariance matrix of each block, say 50 eigenvalues?
block3.eigens <- sapply(1:n.dim, FUN=function(X) rexp(n=1, rate=abs(runif(n=1, min=0, max=50))))
block3.eigens <- block3.eigens[order(block3.eigens)]
block3.p <- qr.Q(qr(matrix(rnorm(block3.cells^2, mean=4, sd=0.01), block3.cells)))
block3.sigma <- crossprod(block3.p, block3.p*block3.eigens)
block3.gex <- abs(rmvnorm(n=r.n, mean=rnorm(n=block3.cells, mean=5, sd=0.01), sigma=block3.sigma))
sim2.gex <- do.call(cbind, list("b1"=block1.gex, "b3"=block3.gex))
sim2.pca <- prcomp_irlba(t(sim2.gex), n=50, scale.=TRUE, center=TRUE)
pairs(sim2.pca$x[, c(1:5)])

I’ll use the reduced dimensions here to compute a KNN-graph and visualise it using a Fructerman-Reingold layout.

set.seed(42)
sim2.knn <- buildKNNGraph(x=sim2.pca$x[, c(1:30)], k=21, d=NA, transposed=TRUE)
sim2.fr.layout <- layout_with_fr(sim2.knn)
plot(sim2.knn, layout=sim2.fr.layout, vertex.frame.color='skyblue', vertex.color='skyblue', vertex.label.color='black', 
     vertex.label.family='Helvetica', edge.color='grey60', vertex.label.cex=0.9,
     vertex.label.dist=1, edge.arrow.size=0.2)

Also a UMAP layout.

set.seed(42)
stem.ta.umap <- umap(sim2.pca$x[, c(1:30)],
                     n_components=2,
                     n_neighbors=21, metric='euclidean',
                     init='random', min_dist=0.1)
plot(stem.ta.umap$layout, col=c(rep("red", block1.cells), rep("orange", block3.cells)),
     xlab="UMAP 1", ylab="UMAP 2")

Within each of these clouds of points I will randomly label 1:9 in block 1 and 1:1 in block 2.

set.seed(42)
block1.cond <- rep("A", block1.cells)
block1.a <- sample(1:block1.cells, size=floor(block1.cells*0.1))
block1.b <- setdiff(1:block1.cells, block1.a)
block1.cond[block1.b] <- "B"
block3.cond <- rep("A", block3.cells)
block3.a <- sample(1:block3.cells, size=floor(block3.cells*0.5))
block3.b <- setdiff(1:block3.cells, block3.a)
block3.cond[block3.b] <- "B"
meta.df <- data.frame("Block"=c(rep("B1", block1.cells), rep("B3", block3.cells)),
                      "Condition"=c(block1.cond, block3.cond),
                      "Replicate"=c(rep("R1", floor(block1.cells*0.33)), rep("R2", floor(block1.cells*0.33)), 
                                    rep("R3", block1.cells-(2*floor(block1.cells*0.33))),
                                    rep("R1", floor(block3.cells*0.33)), rep("R2", floor(block3.cells*0.33)), 
                                    rep("R3", block3.cells-(2*floor(block3.cells*0.33)))))
meta.df <- cbind(meta.df, stem.ta.umap$layout)
colnames(meta.df) <- c("Block", "Condition", "Replicate", "UMAP1", "UMAP2")
# define a "sample" as teh combination of condition and replicate
meta.df$Sample <- paste(meta.df$Condition, meta.df$Replicate, sep="_")
meta.df$Vertex <- c(1:nrow(meta.df))
ggplot(meta.df, aes(x=UMAP1, y=UMAP2)) +
  geom_point(aes(colour=Block, shape=Replicate)) +
  theme_clean() +
  scale_colour_npg() +
  facet_wrap(~Condition) +
  guides(colour=guide_legend(override.aes=list(size=3)),
         shape=guide_legend(override.aes=list(size=3)))

The refined sampling scheme leads to large neighbourhoods overall - we think this might increase power and sensitivity as the counts in each will also be larger and therefore more stable.

Test using random sampling

sim2.counts <- quant_neighbourhood(graph=sim2.knn, meta=meta.df, sample.column='Sample', sample.vertices=n.hood)
sample.meta <- data.frame("Condition"=c(rep("A", 3), rep("B", 3)),
                          "Replicate"=rep(c("R1", "R2", "R3"), 2))
sample.meta$Sample <- paste(sample.meta$Condition, sample.meta$Replicate, sep="_")
rownames(sample.meta) <- sample.meta$Sample
# sim2.model <- model.matrix(~ 0 + Condition, data=sample.meta)
sim2.model <- model.matrix(~ Condition, data=sample.meta)
head(sim2.model)
     (Intercept) ConditionB
A_R1           1          0
A_R2           1          0
A_R3           1          0
B_R1           1          1
B_R2           1          1
B_R3           1          1

I have a model matrix and counts matrix - let’s test edgeR on these.

count.means <- rowMeans(sim2.counts[, rownames(sim2.model)])
count.vars <- apply(sim2.counts[, rownames(sim2.model)], 1, var)
plot(count.means, count.vars)

The data are overdispersed. The model normalisation is causing some consternation. These all rely on normalising the neighbourhood counts by some factor. What if the normalisation uses the total number of cells in the experiment for each sample, rather than the counts in neighbourhoods, which will always be higher because cells are counted multiple times.

sim2.dge <- DGEList(sim2.counts[, rownames(sim2.model)], lib.size=log(colSums(sim2.counts[, rownames(sim2.model)])))
sim2.dge <- estimateDisp(sim2.dge, sim2.model)
sim2.fit <- glmQLFit(sim2.dge, sim2.model, robust=TRUE)
sim2.res <- as.data.frame(topTags(glmQLFTest(sim2.fit, coef=2), sort.by='none', n=Inf))
sim2.res$Neighbourhood <- as.numeric(rownames(sim2.res))
sim2.spatialfdr <- graph_spatialFDR(neighborhoods=vertex.list, graph=sim2.knn, connectivity="distance",
                                    pvalues=sim2.res[order(sim2.res$Neighbourhood), ]$PValue, 
                                    pca=sim2.pca$x[, c(1:30)])
sim2.res$SpatialFDR[order(sim2.res$Neighbourhood)] <- sim2.spatialfdr
qvals <- sim2.spatialfdr
is.sig <- qvals <= 0.01
summary(is.sig)
   Mode   FALSE    TRUE 
logical      29      21 

This is at a 1% FDR.

sim2.neighbour.exprs <- neighborhood_expression(vertex.list, sim2.gex)

Embed these hyperspheres with a PCA and UMAP.

sim2.neighbour.pca <- prcomp((t(sim2.neighbour.exprs)))
set.seed(42)
neighbourhood.umap <- umap(sim2.neighbour.pca$x[, c(1:30)],
                           n_components=2,
                           n_neighbors=21, metric='euclidean',
                           init='random', min_dist=0.1)
plot(neighbourhood.umap$layout,
     xlab="UMAP 1", ylab="UMAP 2")

We can overlay the DA testing on these neighbourhoods.

neighbor.df <- sim2.res[, c("logFC", "Neighbourhood", "SpatialFDR")]
neighbor.df <- do.call(cbind.data.frame, list(neighbor.df, as.data.frame(neighbourhood.umap$layout)))
colnames(neighbor.df) <- c("logFC", "Neighbourhood", "SpatialFDR", "UMAP1", "UMAP2")
neighbor.df$Sig <- as.numeric(neighbor.df$SpatialFDR <= 0.01)
ggplot(neighbor.df, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=neighbor.df[neighbor.df$Sig == 0, ],
             colour='grey80', size=2) +
  geom_point(data=neighbor.df[neighbor.df$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red")

Is that a subtle compositional effect in the 1 neighbourhood that is depleted? That cluster should not contain any DA neighbourhoods.

neighbour.list <- list()
for(x in seq_along(1:length(vertex.list))){
  x.df <- meta.df[meta.df$Vertex %in% vertex.list[[x]], ]
  x.rep <- names(table(x.df$Replicate))[which(table(x.df$Replicate) == max(table(x.df$Replicate)))]
  if(length(x.rep) > 1){
    x.rep <- sample(size=1, x.rep)
  }
  x.block <- names(table(x.df$Block))[which(table(x.df$Block) == max(table(x.df$Block)))]
    if(length(x.block) > 1){
    x.block <- sample(size=1, x.block)
  }
  x.condition <- names(table(x.df$Condition))[which(table(x.df$Condition) == max(table(x.df$Condition)))]
    if(length(x.condition) > 1){
    x.condition <- sample(size=1, x.condition)
  }
  
  neighbour.list[[x]] <- data.frame("Replicate"=x.rep, "Block"=x.block, "Condition"=x.condition, "Neighbourhood"=x)
}
neighbour.meta <- do.call(rbind.data.frame, neighbour.list)
neighbour.merge <- merge(neighbor.df, neighbour.meta, by='Neighbourhood')
neighbour.merge$Block <- ordered(neighbour.merge$Block,
                                 levels=c("B1", "B3"))
neighbour.merge$Diff <- sign(neighbour.merge$logFC)
neighbour.merge$Diff[neighbour.merge$Sig == 0] <- 0
ggplot(neighbour.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=neighbour.merge[, c("UMAP1", "UMAP2")],
             colour='grey80', size=1) +
  geom_point(data=neighbour.merge[neighbour.merge$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red") +
  facet_wrap(~Block)

This doesn’t make sense - Block 3 shouldn’t have any DA neighbourhoods. Is this a compositional effect we’re seeing here? It’s strange that a random fluctuation would cause this - it must be incredibly sensitive. This is also sample-size dependent, smaller total sample sizes are less susceptible for some reason.

table(neighbour.merge$Block, neighbour.merge$Diff)
    
     -1  0  1
  B1  0  0 20
  B3  1 29  0

That single depleted neighbourhood in B3 is, I think, a compositional effect. Does the refined sampling deal with this in some way, either by having neighbourhoods with larger, and thus more stable counts?

all.samps <- unique(paste(meta.df$Block, meta.df$Condition, meta.df$Replicate, sep="_"))
meta.df$All.Sample <- paste(meta.df$Block, meta.df$Condition, meta.df$Replicate, sep="_")
all.count.matrix <- matrix(0L, ncol=length(all.samps), nrow=length(vertex.list))
colnames(all.count.matrix) <- all.samps
  
for(x in seq_along(1:length(vertex.list))){
  v.x <- vertex.list[[x]]
  for(i in seq_along(1:length(all.samps))){
    i.s <- all.samps[i]
    i.s.vertices <- intersect(v.x, meta.df[meta.df$All.Sample == i.s, ]$Vertex)
    all.count.matrix[x, i] <- length(i.s.vertices)
  }
}
all.count.melt <- melt(all.count.matrix)
all.count.melt$Var2 <- as.character(all.count.melt$Var2)
all.count.melt$Block <- unlist(lapply(strsplit(all.count.melt$Var2, split="_", fixed=TRUE),
                                      FUN=function(XP) paste0(XP[1])))
all.count.melt$Condition <- unlist(lapply(strsplit(all.count.melt$Var2, split="_", fixed=TRUE),
                                      FUN=function(XP) paste0(XP[2])))
all.count.melt$Replicate <- unlist(lapply(strsplit(all.count.melt$Var2, split="_", fixed=TRUE),
                                      FUN=function(XP) paste0(XP[3])))
ggplot(all.count.melt[all.count.melt$Var1 %in% c(neighbour.merge$Neighbourhood[neighbour.merge$Sig == 1]) &
                        all.count.melt$Block %in% c("B3"), ],
       aes(x=Block, y=value, fill=Condition)) +
  geom_boxplot() +
  theme_clean() +
  facet_wrap(~Var1, scales="free_y")

These are the counts for B3 in the DANs. N21 looks like the effect is from sampling variance, as the counts are really quite low.

Test using refined sampling

refine_vertex <- function(vertex.knn, v.ix, X_pca){
  # vertex.knn: KNN graph for randomly sampled points (output of BiocNeighbors::findKNN)
  # v.ix: index of vertex to refine in vertex.knn
  
  ## Calculate median profile of KNNs of vertex
  v.med <- apply(X_pca[vertex.knn$index[v.ix,],], 2, median)
  ## Find the closest point to the median and sample
  refined.vertex <- BiocNeighbors::findKNN(rbind(v.med, X_pca), subset=1, k=1)[["index"]][1] - 1 ## -1 to remove the median
  return(refined.vertex)
}
quant_neighbourhood <- function(graph, meta, sample.column='Sample', sample.vertices=0.25, seed=42, pca=NULL, sample="random"){
  set.seed(seed)
  
  if(sample == "random"){
  # define a set of vertices and neihbourhood centers - extract the neihbourhoods of these cells
  random.vertices <- sample(V(graph), size=floor(sample.vertices*length(V(graph))))
  vertex.list <- sapply(1:length(random.vertices), FUN=function(X) neighbors(graph, v=random.vertices[X]))
  } else if(sample == "refined"){
    if(is.null(pca)){
      stop("Please pass a PCA object - expected output from prcomp()")
    }
    X_pca <- pca$x[, c(1:30)]
    
    random.vertices <- sample(V(graph), size=floor(sample.vertices*length(V(graph))))
    vertex.knn <- BiocNeighbors::findKNN(X=X_pca, k=21, subset=as.vector(random.vertices))
    refined.vertices <- V(graph)[sapply(1:nrow(vertex.knn$index), function(i) refine_vertex(vertex.knn, i, X_pca))]
  
    vertex.list <- sapply(1:length(random.vertices), FUN=function(X) neighbors(graph, v=random.vertices[X]))
    vertex.list.refined <- sapply(1:length(refined.vertices), FUN=function(X) neighbors(graph, v=refined.vertices[X]))  
    vertex.list <- vertex.list.refined
  }
  
  count.matrix <- matrix(0L, ncol=length(unique(meta[, sample.column])), nrow=length(vertex.list))
  colnames(count.matrix) <- unique(meta[, sample.column])
  
  for(x in seq_along(1:length(vertex.list))){
    v.x <- vertex.list[[x]]
    for(i in seq_along(1:length(unique(meta[, sample.column])))){
      i.s <- unique(meta[, sample.column])[i]
      i.s.vertices <- intersect(v.x, meta[meta[, sample.column] == i.s, ]$Vertex)
      count.matrix[x, i] <- length(i.s.vertices)
    }
  }
  rownames(count.matrix) <- c(1:length(vertex.list))
  return(count.matrix)
}
sim2.counts <- quant_neighbourhood(graph=sim2.knn, meta=meta.df, sample.column='Sample', sample.vertices=n.hood, sample="refined", pca=sim2.pca)
sample.meta <- data.frame("Condition"=c(rep("A", 3), rep("B", 3)),
                          "Replicate"=rep(c("R1", "R2", "R3"), 2))
sample.meta$Sample <- paste(sample.meta$Condition, sample.meta$Replicate, sep="_")
rownames(sample.meta) <- sample.meta$Sample
# sim2.model <- model.matrix(~ 0 + Condition, data=sample.meta)
sim2.model <- model.matrix(~ Condition, data=sample.meta)
head(sim2.model)
     (Intercept) ConditionB
A_R1           1          0
A_R2           1          0
A_R3           1          0
B_R1           1          1
B_R2           1          1
B_R3           1          1

I have a model matrix and counts matrix - let’s test edgeR on these.

sim2.dge <- DGEList(sim2.counts[, rownames(sim2.model)], lib.size=log(colSums(sim2.counts[, rownames(sim2.model)])))
sim2.dge <- estimateDisp(sim2.dge, sim2.model, tagwise=TRUE)
sim2.fit <- glmQLFit(sim2.dge, sim2.model, robust=TRUE)
# sim2.contrast <- makeContrasts(ConditionA - ConditionB, levels=sim2.model)
# sim2.res <- glmQLFTest(sim2.fit, contrast=sim2.contrast)
sim2.res <- as.data.frame(topTags(glmQLFTest(sim2.fit, coef=2), sort.by='none', n=Inf))
sim2.res$Neighbourhood <- as.numeric(rownames(sim2.res))
sim2.spatialfdr <- graph_spatialFDR(neighborhoods=vertex.list.refined, graph=sim2.knn, connectivity="distance",
                                    pvalues=sim2.res[order(sim2.res$Neighbourhood), ]$PValue, 
                                    pca=sim2.pca$x[, c(1:30)])
sim2.res$SpatialFDR[order(sim2.res$Neighbourhood)] <- sim2.spatialfdr
qvals <- sim2.spatialfdr
is.sig <- qvals <= 0.01
summary(is.sig)
   Mode   FALSE    TRUE 
logical      30      20 

That’s a lot of DA neigbbourhoods!

sim2.neighbour.exprs <- neighborhood_expression(vertex.list.refined, sim2.gex)

Embed these hyperspheres with a PCA and UMAP.

sim2.neighbour.pca <- prcomp((t(sim2.neighbour.exprs)))
set.seed(42)
neighbourhood.umap <- umap(sim2.neighbour.pca$x[, c(1:30)],
                           n_components=2,
                           n_neighbors=21, metric='euclidean',
                           init='random', min_dist=0.1)
plot(neighbourhood.umap$layout,
     xlab="UMAP 1", ylab="UMAP 2")

We can overlay the DA testing on these neighbourhoods.

neighbor.df <- sim2.res[, c("logFC", "Neighbourhood", "SpatialFDR")]
neighbor.df <- do.call(cbind.data.frame, list(neighbor.df, as.data.frame(neighbourhood.umap$layout)))
colnames(neighbor.df) <- c("logFC", "Neighbourhood", "SpatialFDR", "UMAP1", "UMAP2")
neighbor.df$Sig <- as.numeric(neighbor.df$SpatialFDR <= 0.01)
ggplot(neighbor.df, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=neighbor.df[neighbor.df$Sig == 0, ],
             colour='grey80', size=2) +
  geom_point(data=neighbor.df[neighbor.df$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red")

No more false DANs in B3, despite the same number of neighbourhoods being counted.

neighbour.list <- list()
for(x in seq_along(1:length(vertex.list.refined))){
  x.df <- meta.df[meta.df$Vertex %in% vertex.list.refined[[x]], ]
  x.rep <- names(table(x.df$Replicate))[which(table(x.df$Replicate) == max(table(x.df$Replicate)))]
  if(length(x.rep) > 1){
    x.rep <- sample(size=1, x.rep)
  }
  x.block <- names(table(x.df$Block))[which(table(x.df$Block) == max(table(x.df$Block)))]
    if(length(x.block) > 1){
    x.block <- sample(size=1, x.block)
  }
  x.condition <- names(table(x.df$Condition))[which(table(x.df$Condition) == max(table(x.df$Condition)))]
    if(length(x.condition) > 1){
    x.condition <- sample(size=1, x.condition)
  }
  
  neighbour.list[[x]] <- data.frame("Replicate"=x.rep, "Block"=x.block, "Condition"=x.condition, "Neighbourhood"=x)
}
neighbour.meta <- do.call(rbind.data.frame, neighbour.list)
neighbour.merge <- merge(neighbor.df, neighbour.meta, by='Neighbourhood')
neighbour.merge$Block <- ordered(neighbour.merge$Block,
                                 levels=c("B1", "B3"))
neighbour.merge$Diff <- sign(neighbour.merge$logFC)
neighbour.merge$Diff[neighbour.merge$Sig == 0] <- 0
ggplot(neighbour.merge, aes(x=UMAP1, y=UMAP2)) +
  geom_point(data=neighbour.merge[, c("UMAP1", "UMAP2")],
             colour='grey80', size=1) +
  geom_point(data=neighbour.merge[neighbour.merge$Sig == 1, ],
             aes(colour=logFC), size=4) +
  theme_clean() +
  scale_colour_gradient2(low="blue", mid="grey80", high="red") +
  facet_wrap(~Block)

This doesn’t make sense - Block 3 shouldn’t have any DA neighbourhoods. Is this a compositional effect we’re seeing here? It’s strange that a random fluctuation would cause this - it must be incredibly sensitive.

table(neighbour.merge$Block, neighbour.merge$Diff)
    
      0  1
  B1  0 20
  B3 30  0
all.samps <- unique(paste(meta.df$Block, meta.df$Condition, meta.df$Replicate, sep="_"))
meta.df$All.Sample <- paste(meta.df$Block, meta.df$Condition, meta.df$Replicate, sep="_")
all.count.matrix <- matrix(0L, ncol=length(all.samps), nrow=length(vertex.list.refined))
colnames(all.count.matrix) <- all.samps
  
for(x in seq_along(1:length(vertex.list.refined))){
  v.x <- vertex.list.refined[[x]]
  for(i in seq_along(1:length(all.samps))){
    i.s <- all.samps[i]
    i.s.vertices <- intersect(v.x, meta.df[meta.df$All.Sample == i.s, ]$Vertex)
    all.count.matrix[x, i] <- length(i.s.vertices)
  }
}
all.count.melt <- melt(all.count.matrix)
all.count.melt$Var2 <- as.character(all.count.melt$Var2)
all.count.melt$Block <- unlist(lapply(strsplit(all.count.melt$Var2, split="_", fixed=TRUE),
                                      FUN=function(XP) paste0(XP[1])))
all.count.melt$Condition <- unlist(lapply(strsplit(all.count.melt$Var2, split="_", fixed=TRUE),
                                      FUN=function(XP) paste0(XP[2])))
all.count.melt$Replicate <- unlist(lapply(strsplit(all.count.melt$Var2, split="_", fixed=TRUE),
                                      FUN=function(XP) paste0(XP[3])))
ggplot(all.count.melt[all.count.melt$Var1 %in% c(neighbour.merge$Neighbourhood[neighbour.merge$Sig == 1]) &
                        all.count.melt$Block %in% c("B3"), ],
       aes(x=Block, y=value, fill=Condition)) +
  geom_boxplot() +
  theme_clean() +
  facet_wrap(~Var1, scales="free_y")

New buildKNNGraph() to retain cell-cell distances

If we want to base the spatial FDR correction on a distance then we need some way to retain this information in the graph to speed-up the calculations for large and highly-connected graphs. Our idea is to re-code the buildKNNGraph() function to retain cell-cell distances in the edge weights slot of the igraph object.

# set.seed(42)
# sim2.knn <- .buildKNNGraph(x=sim2.pca$x[, c(1:30)], k=21, d=NA, transposed=TRUE)
# sim2.fr.layout <- layout_with_fr(sim2.knn)
# plot(sim2.knn, layout=sim2.fr.layout, vertex.frame.color='skyblue', vertex.color='skyblue', vertex.label.color='black',
#      vertex.label.family='Helvetica', edge.color='grey60', vertex.label.cex=0.9,
#      vertex.label.dist=1, edge.arrow.size=0.2)

NB: Storing the distances as edge weights is far too memory intensive. Perhaps working off the back of the SingleCellExperiment would be better from a design point of view.

LS0tCnRpdGxlOiAiTWlsbzogU2ltdWxhdGlvbnMgZm9yIGRpc2NyZXRlIGNhc2VzIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgojIEludHJvZHVjdGlvbgoKR2VuZXJhdGluZyBzaW11bGF0aW9ucyBhbmQgdGhlbiB1c2luZyB0aGUgdGh5bXVzIGFnZWluZyBkYXRhIHRvIHRlc3QgZGlmZmVyZW50aWFsIGFidW5kYW5jZSB0ZXN0aW5nIG9uIGEgZ3JhcGgvdHJlZS4KCmBgYHtyLCBlY2hvPVRSVUUsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9CmxpYnJhcnkoZ2dwbG90MikKbGlicmFyeShlZGdlUikKbGlicmFyeShpZ3JhcGgpCmxpYnJhcnkoU2luZ2xlQ2VsbEV4cGVyaW1lbnQpCmxpYnJhcnkoc2NyYW4pCmxpYnJhcnkoc2NhdGVyKQpsaWJyYXJ5KGlybGJhKQpsaWJyYXJ5KGdndGhlbWVzKQpsaWJyYXJ5KGdnc2NpKQpsaWJyYXJ5KGN5ZGFyKQpsaWJyYXJ5KG12dG5vcm0pCmxpYnJhcnkodW1hcCkKbGlicmFyeShyZXNoYXBlMikKYGBgCgojIyBTaW11bGF0aW9uIDEKCkknbGwgc3RhcnQgYnkgc2ltdWxhdGluZyAzIGNsb3VkcyBvZiBwb2ludHMgaW4gJFxtYXRoYmJ7Un1ee259JCwgZWFjaCBjb25zaXN0aW5nIG9mIHBvaW50cyBmcm9tIDIgcG9vbHMsIEEgJiBCLiBFYWNoIGNsb3VkIG9mIHBvaW50cyB3aWxsIGJlIGNvbXBvc2VkIG9mOgoKKiBBOiAxMCUsIEI6IDkwJQoqIEE6IDkwJSwgQjogMTAlCiogQTogNTAlLCBCOiA1MCUKCkkgd2lsbCB0aGVuIHBlcmZvcm0gYSBQQ0Egb24gdGhlIG1hdHJpeCBvZiB0aGVzZSBwb2ludHMgYW5kIGNvbnN0cnVjdCBhIEtOTi1ncmFwaCwgbWltaWNpbmcgdGhlIHN0YW5kYXJkIHdvcmtmbG93IG9mIG1hbnkgc2NSTkEtc2VxIGFuYWx5c2VzLgoKYGBge3IsIGVjaG89VFJVRSwgd2FybmluZz1GQUxTRX0Kc2V0LnNlZWQoNDIpCnIubiA8LSAxMDAwCm4uZGltIDwtIDUwCmJsb2NrMS5jZWxscyA8LSAxMjAwCiMgc2VsZWN0IGEgc2V0IG9mIGVpZ2VuIHZhbHVlcyBmb3IgdGhlIGNvdmFyaWFuY2UgbWF0cml4IG9mIGVhY2ggYmxvY2ssIHNheSA1MCBlaWdlbnZhbHVlcz8KYmxvY2sxLmVpZ2VucyA8LSBzYXBwbHkoMTpuLmRpbSwgRlVOPWZ1bmN0aW9uKFgpIHJleHAobj0xLCByYXRlPWFicyhydW5pZihuPTEsIG1pbj0wLCBtYXg9NTApKSkpCmJsb2NrMS5laWdlbnMgPC0gYmxvY2sxLmVpZ2Vuc1tvcmRlcihibG9jazEuZWlnZW5zKV0KYmxvY2sxLnAgPC0gcXIuUShxcihtYXRyaXgocm5vcm0oYmxvY2sxLmNlbGxzXjIsIG1lYW49NCwgc2Q9MC4wMSksIGJsb2NrMS5jZWxscykpKQpibG9jazEuc2lnbWEgPC0gY3Jvc3Nwcm9kKGJsb2NrMS5wLCBibG9jazEucCpibG9jazEuZWlnZW5zKQpibG9jazEuZ2V4IDwtIGFicyhybXZub3JtKG49ci5uLCBtZWFuPXJub3JtKG49YmxvY2sxLmNlbGxzLCBtZWFuPTIsIHNkPTAuMDEpLCBzaWdtYT1ibG9jazEuc2lnbWEpKQoKCmJsb2NrMi5jZWxscyA8LSAxMjAwCiMgc2VsZWN0IGEgc2V0IG9mIGVpZ2VuIHZhbHVlcyBmb3IgdGhlIGNvdmFyaWFuY2UgbWF0cml4IG9mIGVhY2ggYmxvY2ssIHNheSA1MCBlaWdlbnZhbHVlcz8KYmxvY2syLmVpZ2VucyA8LSBzYXBwbHkoMTpuLmRpbSwgRlVOPWZ1bmN0aW9uKFgpIHJleHAobj0xLCByYXRlPWFicyhydW5pZihuPTEsIG1pbj0wLCBtYXg9NTApKSkpCmJsb2NrMi5laWdlbnMgPC0gYmxvY2syLmVpZ2Vuc1tvcmRlcihibG9jazIuZWlnZW5zKV0KYmxvY2syLnAgPC0gcXIuUShxcihtYXRyaXgocm5vcm0oYmxvY2syLmNlbGxzXjIsIG1lYW49NCwgc2Q9MC4wMSksIGJsb2NrMi5jZWxscykpKQpibG9jazIuc2lnbWEgPC0gY3Jvc3Nwcm9kKGJsb2NrMi5wLCBibG9jazIucCpibG9jazIuZWlnZW5zKQpibG9jazIuZ2V4IDwtIGFicyhybXZub3JtKG49ci5uLCBtZWFuPXJub3JtKG49YmxvY2syLmNlbGxzLCBtZWFuPTQsIHNkPTAuMDEpLCBzaWdtYT1ibG9jazIuc2lnbWEpKQoKCmJsb2NrMy5jZWxscyA8LSAxMjUwCiMgc2VsZWN0IGEgc2V0IG9mIGVpZ2VuIHZhbHVlcyBmb3IgdGhlIGNvdmFyaWFuY2UgbWF0cml4IG9mIGVhY2ggYmxvY2ssIHNheSA1MCBlaWdlbnZhbHVlcz8KYmxvY2szLmVpZ2VucyA8LSBzYXBwbHkoMTpuLmRpbSwgRlVOPWZ1bmN0aW9uKFgpIHJleHAobj0xLCByYXRlPWFicyhydW5pZihuPTEsIG1pbj0wLCBtYXg9NTApKSkpCmJsb2NrMy5laWdlbnMgPC0gYmxvY2szLmVpZ2Vuc1tvcmRlcihibG9jazMuZWlnZW5zKV0KYmxvY2szLnAgPC0gcXIuUShxcihtYXRyaXgocm5vcm0oYmxvY2szLmNlbGxzXjIsIG1lYW49NCwgc2Q9MC4wMSksIGJsb2NrMy5jZWxscykpKQpibG9jazMuc2lnbWEgPC0gY3Jvc3Nwcm9kKGJsb2NrMy5wLCBibG9jazMucCpibG9jazMuZWlnZW5zKQpibG9jazMuZ2V4IDwtIGFicyhybXZub3JtKG49ci5uLCBtZWFuPXJub3JtKG49YmxvY2szLmNlbGxzLCBtZWFuPTUsIHNkPTAuMDEpLCBzaWdtYT1ibG9jazMuc2lnbWEpKQoKc2ltMS5nZXggPC0gZG8uY2FsbChjYmluZCwgbGlzdCgiYjEiPWJsb2NrMS5nZXgsICJiMiI9YmxvY2syLmdleCwgImIzIj1ibG9jazMuZ2V4KSkKYGBgCgoKYGBge3J9CnNpbTEucGNhIDwtIHByY29tcF9pcmxiYSh0KHNpbTEuZ2V4KSwgbj01MCwgc2NhbGUuPVRSVUUsIGNlbnRlcj1UUlVFKQpwYWlycyhzaW0xLnBjYSR4WywgYygxOjUpXSkKYGBgCgpJJ2xsIHVzZSB0aGUgcmVkdWNlZCBkaW1lbnNpb25zIGhlcmUgdG8gY29tcHV0ZSBhIEtOTi1ncmFwaCBhbmQgdmlzdWFsaXNlIGl0IHVzaW5nIGEgRnJ1Y3Rlcm1hbi1SZWluZ29sZCBsYXlvdXQuCgpgYGB7cn0Kc2V0LnNlZWQoNDIpCnNpbTEua25uIDwtIGJ1aWxkS05OR3JhcGgoeD1zaW0xLnBjYSR4WywgYygxOjMwKV0sIGs9MjEsIGQ9TkEsIHRyYW5zcG9zZWQ9VFJVRSkKc2ltMS5mci5sYXlvdXQgPC0gbGF5b3V0X3dpdGhfZnIoc2ltMS5rbm4pCnBsb3Qoc2ltMS5rbm4sIGxheW91dD1zaW0xLmZyLmxheW91dCwgdmVydGV4LmZyYW1lLmNvbG9yPSdza3libHVlJywgdmVydGV4LmNvbG9yPSdza3libHVlJywgdmVydGV4LmxhYmVsLmNvbG9yPSdibGFjaycsIAogICAgIHZlcnRleC5sYWJlbC5mYW1pbHk9J0hlbHZldGljYScsIGVkZ2UuY29sb3I9J2dyZXk2MCcsIHZlcnRleC5sYWJlbC5jZXg9MC45LAogICAgIHZlcnRleC5sYWJlbC5kaXN0PTEsIGVkZ2UuYXJyb3cuc2l6ZT0wLjIpCmBgYAoKQWxzbyBhIFVNQVAgbGF5b3V0LgoKYGBge3J9CnNldC5zZWVkKDQyKQpzdGVtLnRhLnVtYXAgPC0gdW1hcChzaW0xLnBjYSR4WywgYygxOjMwKV0sCiAgICAgICAgICAgICAgICAgICAgIG5fY29tcG9uZW50cz0yLAogICAgICAgICAgICAgICAgICAgICBuX25laWdoYm9ycz0yMSwgbWV0cmljPSdldWNsaWRlYW4nLAogICAgICAgICAgICAgICAgICAgICBpbml0PSdyYW5kb20nLCBtaW5fZGlzdD0wLjEpCnBsb3Qoc3RlbS50YS51bWFwJGxheW91dCwgY29sPWMocmVwKCJyZWQiLCBibG9jazEuY2VsbHMpLCByZXAoInNreWJsdWUiLCBibG9jazIuY2VsbHMpLCByZXAoIm9yYW5nZSIsIGJsb2NrMy5jZWxscykpLAogICAgIHhsYWI9IlVNQVAgMSIsIHlsYWI9IlVNQVAgMiIpCmBgYAoKV2l0aGluIGVhY2ggb2YgdGhlc2UgY2xvdWRzIG9mIHBvaW50cyBJIHdpbGwgcmFuZG9tbHkgbGFiZWwgaW4gdGhlIHByb3BvcnRpb25zIGFib3ZlLgoKYGBge3J9CnNldC5zZWVkKDQyKQpibG9jazEuY29uZCA8LSByZXAoIkEiLCBibG9jazEuY2VsbHMpCmJsb2NrMS5hIDwtIHNhbXBsZSgxOmJsb2NrMS5jZWxscywgc2l6ZT1mbG9vcihibG9jazEuY2VsbHMqMC45KSkKYmxvY2sxLmIgPC0gc2V0ZGlmZigxOmJsb2NrMS5jZWxscywgYmxvY2sxLmEpCmJsb2NrMS5jb25kW2Jsb2NrMS5iXSA8LSAiQiIKCmJsb2NrMi5jb25kIDwtIHJlcCgiQSIsIGJsb2NrMi5jZWxscykKYmxvY2syLmEgPC0gc2FtcGxlKDE6YmxvY2syLmNlbGxzLCBzaXplPWZsb29yKGJsb2NrMi5jZWxscyowLjA1KSkKYmxvY2syLmIgPC0gc2V0ZGlmZigxOmJsb2NrMi5jZWxscywgYmxvY2syLmEpCmJsb2NrMi5jb25kW2Jsb2NrMi5iXSA8LSAiQiIKCmJsb2NrMy5jb25kIDwtIHJlcCgiQSIsIGJsb2NrMy5jZWxscykKYmxvY2szLmEgPC0gc2FtcGxlKDE6YmxvY2szLmNlbGxzLCBzaXplPWZsb29yKGJsb2NrMy5jZWxscyowLjUpKQpibG9jazMuYiA8LSBzZXRkaWZmKDE6YmxvY2szLmNlbGxzLCBibG9jazMuYSkKYmxvY2szLmNvbmRbYmxvY2szLmJdIDwtICJCIgoKbWV0YS5kZiA8LSBkYXRhLmZyYW1lKCJCbG9jayI9YyhyZXAoIkIxIiwgYmxvY2sxLmNlbGxzKSwgcmVwKCJCMiIsIGJsb2NrMi5jZWxscyksIHJlcCgiQjMiLCBibG9jazMuY2VsbHMpKSwKICAgICAgICAgICAgICAgICAgICAgICJDb25kaXRpb24iPWMoYmxvY2sxLmNvbmQsIGJsb2NrMi5jb25kLCBibG9jazMuY29uZCksCiAgICAgICAgICAgICAgICAgICAgICAiUmVwbGljYXRlIj1jKHJlcCgiUjEiLCBmbG9vcihibG9jazEuY2VsbHMqMC4zMykpLCByZXAoIlIyIiwgZmxvb3IoYmxvY2sxLmNlbGxzKjAuMzMpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcCgiUjMiLCBibG9jazEuY2VsbHMtKDIqZmxvb3IoYmxvY2sxLmNlbGxzKjAuMzMpKSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcCgiUjEiLCBmbG9vcihibG9jazIuY2VsbHMqMC4zMykpLCByZXAoIlIyIiwgZmxvb3IoYmxvY2syLmNlbGxzKjAuMzMpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcCgiUjMiLCBibG9jazIuY2VsbHMtKDIqZmxvb3IoYmxvY2syLmNlbGxzKjAuMzMpKSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcCgiUjEiLCBmbG9vcihibG9jazMuY2VsbHMqMC4zMykpLCByZXAoIlIyIiwgZmxvb3IoYmxvY2szLmNlbGxzKjAuMzMpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcCgiUjMiLCBibG9jazMuY2VsbHMtKDIqZmxvb3IoYmxvY2szLmNlbGxzKjAuMzMpKSkpKQptZXRhLmRmIDwtIGNiaW5kKG1ldGEuZGYsIHN0ZW0udGEudW1hcCRsYXlvdXQpCmNvbG5hbWVzKG1ldGEuZGYpIDwtIGMoIkJsb2NrIiwgIkNvbmRpdGlvbiIsICJSZXBsaWNhdGUiLCAiVU1BUDEiLCAiVU1BUDIiKQojIGRlZmluZSBhICJzYW1wbGUiIGFzIHRlaCBjb21iaW5hdGlvbiBvZiBjb25kaXRpb24gYW5kIHJlcGxpY2F0ZQptZXRhLmRmJFNhbXBsZSA8LSBwYXN0ZShtZXRhLmRmJENvbmRpdGlvbiwgbWV0YS5kZiRSZXBsaWNhdGUsIHNlcD0iXyIpCm1ldGEuZGYkVmVydGV4IDwtIGMoMTpucm93KG1ldGEuZGYpKQpgYGAKCgpgYGB7cn0KZ2dwbG90KG1ldGEuZGYsIGFlcyh4PVVNQVAxLCB5PVVNQVAyKSkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG91cj1CbG9jaywgc2hhcGU9UmVwbGljYXRlKSkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9ucGcoKSArCiAgZmFjZXRfd3JhcCh+Q29uZGl0aW9uKSArCiAgZ3VpZGVzKGNvbG91cj1ndWlkZV9sZWdlbmQob3ZlcnJpZGUuYWVzPWxpc3Qoc2l6ZT0zKSksCiAgICAgICAgIHNoYXBlPWd1aWRlX2xlZ2VuZChvdmVycmlkZS5hZXM9bGlzdChzaXplPTMpKSkKYGBgCgpVc2luZyB0aGVzZSBzaW11bGF0ZWQgZGF0YSB3ZSBjYW4gZGVmaW5lIGxvdHMgb2YgbmVpZ2hib3VyaG9vZHMgYWNyb3NzIHRoZSBncmFwaCB0byBjcmVhdGUgYSBjb3VudHMgbWF0cml4IG9mIG5laWdoYm91cmhvb2RzIHZzIGNvbmRpdGlvbnMgCmluIGEgc2ltaWxhciB3YXkgYXMgYGN5ZGFyYC4gSSdsbCBzdGFydCB3aXRoIHJhbmRvbSB2ZXJ0aWNlcyBpbiB0aGUgZ3JhcGggbWFraW5nIHVwIDUlIG9mIGFsbCBwb2ludHMgYW5kIGV4dHJhY3QgdGhlIGdyYXBoIG5laWdoYm9yaG9vZHMuCgpgYGB7cn0KIyByYW5kb21seSBzZWxlY3QgdmVydGljZXMgaW4gdGhlIGdyYXBoCm4uaG9vZCA8LSAwLjA1CnJhbmRvbS52ZXJ0aWNlcyA8LSBzYW1wbGUoVihzaW0xLmtubiksIHNpemU9Zmxvb3Iobi5ob29kKmxlbmd0aChWKHNpbTEua25uKSkpKQojIGxvb3Agb3ZlciByYW5kb20gdmVydGljZXMgYW5kIGNvdW50IHRoZSBudW1iZXIgb2YgY2VsbHMgaW4gZWFjaAp2ZXJ0ZXgubGlzdCA8LSBzYXBwbHkoMTpsZW5ndGgocmFuZG9tLnZlcnRpY2VzKSwgRlVOPWZ1bmN0aW9uKFgpIG5laWdoYm9ycyhzaW0xLmtubiwgdj1yYW5kb20udmVydGljZXNbWF0pKQpoaXN0KHVubGlzdChsYXBwbHkodmVydGV4Lmxpc3QsIGxlbmd0aCkpLCAxMDAsIG1haW49Ikhpc3RvZ3JhbSBvZiBuZWlnaGJvcnMiLCB4bGFiPSJOZWlnaGJvdXJob29kIHNpemUiKQpgYGAKCkZvciBlYWNoIG5laWdoYm91cmhvb2QgSSdsbCBjb3VudCB0aGUgbnVtYmVyIG9mIGNlbGxzIGluIGVhY2gsIGRldGVybWluZWQgYnkgdGhlIGV4cGVyaW1lbnRhbCBkZXNpZ24sIGkuZS4gcmVwbGljYXRlLCBjb25kaXRpb24gYW5kIGJsb2NrLgoKYGBge3J9CnF1YW50X25laWdoYm91cmhvb2QgPC0gZnVuY3Rpb24oZ3JhcGgsIG1ldGEsIHNhbXBsZS5jb2x1bW49J1NhbXBsZScsIHNhbXBsZS52ZXJ0aWNlcz0wLjI1LCBzZWVkPTQyKXsKICBzZXQuc2VlZChzZWVkKQogICMgZGVmaW5lIGEgc2V0IG9mIHZlcnRpY2VzIGFuZCBuZWloYm91cmhvb2QgY2VudGVycyAtIGV4dHJhY3QgdGhlIG5laWhib3VyaG9vZHMgb2YgdGhlc2UgY2VsbHMKICByYW5kb20udmVydGljZXMgPC0gc2FtcGxlKFYoZ3JhcGgpLCBzaXplPWZsb29yKHNhbXBsZS52ZXJ0aWNlcypsZW5ndGgoVihncmFwaCkpKSkKICB2ZXJ0ZXgubGlzdCA8LSBzYXBwbHkoMTpsZW5ndGgocmFuZG9tLnZlcnRpY2VzKSwgRlVOPWZ1bmN0aW9uKFgpIG5laWdoYm9ycyhncmFwaCwgdj1yYW5kb20udmVydGljZXNbWF0pKQogIAogIGNvdW50Lm1hdHJpeCA8LSBtYXRyaXgoMEwsIG5jb2w9bGVuZ3RoKHVuaXF1ZShtZXRhWywgc2FtcGxlLmNvbHVtbl0pKSwgbnJvdz1sZW5ndGgodmVydGV4Lmxpc3QpKQogIGNvbG5hbWVzKGNvdW50Lm1hdHJpeCkgPC0gdW5pcXVlKG1ldGFbLCBzYW1wbGUuY29sdW1uXSkKICAKICBmb3IoeCBpbiBzZXFfYWxvbmcoMTpsZW5ndGgodmVydGV4Lmxpc3QpKSl7CiAgICB2LnggPC0gdmVydGV4Lmxpc3RbW3hdXQogICAgZm9yKGkgaW4gc2VxX2Fsb25nKDE6bGVuZ3RoKHVuaXF1ZShtZXRhWywgc2FtcGxlLmNvbHVtbl0pKSkpewogICAgICBpLnMgPC0gdW5pcXVlKG1ldGFbLCBzYW1wbGUuY29sdW1uXSlbaV0KICAgICAgaS5zLnZlcnRpY2VzIDwtIGludGVyc2VjdCh2LngsIG1ldGFbbWV0YVssIHNhbXBsZS5jb2x1bW5dID09IGkucywgXSRWZXJ0ZXgpCiAgICAgIGNvdW50Lm1hdHJpeFt4LCBpXSA8LSBsZW5ndGgoaS5zLnZlcnRpY2VzKQogICAgfQogIH0KICByb3duYW1lcyhjb3VudC5tYXRyaXgpIDwtIGMoMTpsZW5ndGgodmVydGV4Lmxpc3QpKQogIHJldHVybihjb3VudC5tYXRyaXgpCn0KYGBgCgoKYGBge3J9CiMgZGVmaW5lIGEgc2V0IG9mIHZlcnRpY2VzIGFuZCBuZWloYm91cmhvb2QgY2VudGVycyAtIGV4dHJhY3QgdGhlIG5laWhib3VyaG9vZHMgb2YgdGhlc2UgY2VsbHMKc2V0LnNlZWQoNDIpCnJhbmRvbS52ZXJ0aWNlcyA8LSBzYW1wbGUoVihzaW0xLmtubiksIHNpemU9Zmxvb3Iobi5ob29kKmxlbmd0aChWKHNpbTEua25uKSkpKQp2ZXJ0ZXgubGlzdCA8LSBzYXBwbHkoMTpsZW5ndGgocmFuZG9tLnZlcnRpY2VzKSwgRlVOPWZ1bmN0aW9uKFgpIG5laWdoYm9ycyhzaW0xLmtubiwgdj1yYW5kb20udmVydGljZXNbWF0pKQoKY291bnQubWF0cml4IDwtIG1hdHJpeCgwTCwgbmNvbD1sZW5ndGgodW5pcXVlKG1ldGEuZGZbLCAiU2FtcGxlIl0pKSwgbnJvdz1sZW5ndGgodmVydGV4Lmxpc3QpKQpjb2xuYW1lcyhjb3VudC5tYXRyaXgpIDwtIHVuaXF1ZShtZXRhLmRmWywgIlNhbXBsZSJdKQoKZm9yKHggaW4gc2VxX2Fsb25nKDE6bGVuZ3RoKHZlcnRleC5saXN0KSkpewogIHYueCA8LSB2ZXJ0ZXgubGlzdFtbeF1dCiAgZm9yKGkgaW4gc2VxX2Fsb25nKDE6bGVuZ3RoKHVuaXF1ZShtZXRhLmRmWywgIlNhbXBsZSJdKSkpKXsKICAgIGkucyA8LSB1bmlxdWUobWV0YS5kZlssICJTYW1wbGUiXSlbaV0KICAgIGkucy52ZXJ0aWNlcyA8LSBpbnRlcnNlY3Qodi54LCBtZXRhLmRmW21ldGEuZGZbLCAiU2FtcGxlIl0gPT0gaS5zLCBdJFZlcnRleCkKICAgIGNvdW50Lm1hdHJpeFt4LCBpXSA8LSBsZW5ndGgoaS5zLnZlcnRpY2VzKQogIH0KfQpyb3duYW1lcyhjb3VudC5tYXRyaXgpIDwtIGMoMTpsZW5ndGgodmVydGV4Lmxpc3QpKQpgYGAKCgpgYGB7cn0Kc2ltMS5jb3VudHMgPC0gcXVhbnRfbmVpZ2hib3VyaG9vZChncmFwaD1zaW0xLmtubiwgbWV0YT1tZXRhLmRmLCBzYW1wbGUuY29sdW1uPSdTYW1wbGUnLCBzYW1wbGUudmVydGljZXM9bi5ob29kKQpzYW1wbGUubWV0YSA8LSBkYXRhLmZyYW1lKCJDb25kaXRpb24iPWMocmVwKCJBIiwgMyksIHJlcCgiQiIsIDMpKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAiUmVwbGljYXRlIj1yZXAoYygiUjEiLCAiUjIiLCAiUjMiKSwgMikpCnNhbXBsZS5tZXRhJFNhbXBsZSA8LSBwYXN0ZShzYW1wbGUubWV0YSRDb25kaXRpb24sIHNhbXBsZS5tZXRhJFJlcGxpY2F0ZSwgc2VwPSJfIikKcm93bmFtZXMoc2FtcGxlLm1ldGEpIDwtIHNhbXBsZS5tZXRhJFNhbXBsZQojIHNpbTEubW9kZWwgPC0gbW9kZWwubWF0cml4KH4gMCArIENvbmRpdGlvbiwgZGF0YT1zYW1wbGUubWV0YSkKc2ltMS5tb2RlbCA8LSBtb2RlbC5tYXRyaXgofiBDb25kaXRpb24sIGRhdGE9c2FtcGxlLm1ldGEpCmhlYWQoc2ltMS5tb2RlbCkKYGBgCgpJIGhhdmUgYSBtb2RlbCBtYXRyaXggYW5kIGNvdW50cyBtYXRyaXggLSBsZXQncyB0ZXN0IGVkZ2VSIG9uIHRoZXNlLgoKYGBge3J9CnNpbTEuZGdlIDwtIERHRUxpc3Qoc2ltMS5jb3VudHNbLCByb3duYW1lcyhzaW0xLm1vZGVsKV0sIGxpYi5zaXplPWxvZyhjb2xTdW1zKHNpbTEuY291bnRzWywgcm93bmFtZXMoc2ltMS5tb2RlbCldKSkpCnNpbTEuZGdlIDwtIGVzdGltYXRlRGlzcChzaW0xLmRnZSwgc2ltMS5tb2RlbCkKc2ltMS5maXQgPC0gZ2xtUUxGaXQoc2ltMS5kZ2UsIHNpbTEubW9kZWwsIHJvYnVzdD1UUlVFKQojIHNpbTEuY29udHJhc3QgPC0gbWFrZUNvbnRyYXN0cyhDb25kaXRpb25BIC0gQ29uZGl0aW9uQiwgbGV2ZWxzPXNpbTEubW9kZWwpCiMgc2ltMS5yZXMgPC0gZ2xtUUxGVGVzdChzaW0xLmZpdCwgY29udHJhc3Q9c2ltMS5jb250cmFzdCkKCnNpbTEucmVzIDwtIGFzLmRhdGEuZnJhbWUodG9wVGFncyhnbG1RTEZUZXN0KHNpbTEuZml0LCBjb2VmPTIpLCBzb3J0LmJ5PSdub25lJywgbj1JbmYpKQpzaW0xLnJlcyRTaWcgPC0gYXMuZmFjdG9yKGFzLm51bWVyaWMoc2ltMS5yZXMkRkRSIDw9IDAuMDEpKQpzaW0xLnJlcyROZWlnaGJvdXJob29kIDwtIGFzLm51bWVyaWMocm93bmFtZXMoc2ltMS5yZXMpKQoKIyBjb250cm9sIHRoZSBzcGF0aWFsIEZEUgpxdmFscyA8LSBzaW0xLnJlcyRQVmFsdWUKaXMuc2lnIDwtIHF2YWxzIDw9IDAuMDEKc3VtbWFyeShpcy5zaWcpCmBgYAoKVGhpcyBpbmRpY2F0ZXMgdGhhdCAxMjMgb2YgdGhlIG5laWdoYm91ciBob29kcyBhcmUgc2lnbmlmaWNhbnQgYmVmb3JlIHNwYXRpYWwgRkRSIGNvcnJlY3Rpb24uIFRoZSBpbXBsZW1lbnRhdGlvbiBpbiBgQ3lkYXJgIHJlbGllcyBvbiBoYXZpbmcgdGhlIAptZWRpYW4gbWFya2VyIGludGVuc2l0aWVzIGZvciBlYWNoIGh5cGVyc3BoZXJlLiBXaGF0IGlzIHRoZSBlcXVpdmFsZW50IHBvc2l0aW9uYWwgaW5mb3JtYXRpb24gaGVyZT8gV2hhdCBhYm91dCBkcmF3aW5nIGEgbmV3IGdyYXBoIGZvciBlYWNoIApuZWlnaGJvcmhvb2QgdXNpbmcgdGhlIGBpZ3JhcGg6OmluZHVjZWRfc3ViZ3JhcGhgIGZ1bmN0aW9uLCB0aGVuIGNhbGN1bGF0ZSB0aGUgY29ubmVjdGl2aXR5IG9mIHRoaXMgbmV3IHN1Yi1ncmFwaCBhcyBhIG1lYXN1cmUgb2YgdGhlIApuZWlnaGJvcmhvb2QgZGVuc2l0eT8KCmBgYHtyfQojIHRoaXMgY3JlYXRlcyBhIHN1Yi1ncmFwaCBmb3IgZWFjaCBvZiB0aGUgcmFuZG9tIHZlcnRpY2VzCnRlc3Quc3ViZ3JhcGhzIDwtIGxhcHBseSgxOmxlbmd0aChyYW5kb20udmVydGljZXMpLAogICAgICAgICAgICAgICAgICAgICAgICAgRlVOPWZ1bmN0aW9uKFgpIGluZHVjZWRfc3ViZ3JhcGgoc2ltMS5rbm4sIHZlcnRleC5saXN0W1tYXV0pKQoKIyBub3cgbG9vcCBvdmVyIHRoZXNlIHN1Yi1ncmFwaHMgdG8gY2FsY3VsYXRlIHRoZSBjb25uZWN0aXZpdHkgLSB0aGlzIHNlZW1zIGEgbGl0dGxlIHNsb3cuLi4KdGVzdC5jb25uZWN0IDwtIGxhcHBseSh0ZXN0LnN1YmdyYXBocywgRlVOPWZ1bmN0aW9uKEVHKSB2ZXJ0ZXhfY29ubmVjdGl2aXR5KEVHKSkKYGBgCgpWZXJ0ZXggY29ubmVjdGl2aXR5IGlzIHNsb3cgdG8gY29tcHV0ZSAtIGVkZ2UgY29ubmVjdGl2aXR5IG1pZ2h0IGJlIGZhc3RlciBhbmQgaGFzIHRoZSB1c2VmdWwgcHJvcGVydHkgdGhhdDoKCiQkayhHKSBcbGVxIFxsYW1iZGEoRykgJCQKVGhhdCBpcywgdGhlIHZlcnRleCBjb25uZWN0aXZpdHkgJGsoRykkIGlzICRcbGVxJCB0aGFuIHRoZSBlZGdlIGNvbm5lY3Rpdml0eSAkXGxhbWJkYShHKSQuIEluIHRoaXMgaW5zdGFuY2UgdGhlIGVkZ2UtY29ubmVjdGl2aXR5IGlzIGFuIAp1cHBlci1ib3VuZCBvbiB0aGUgbmVpZ2hib3Job29kIGNvbm5lY3Rpdml0eSwgYW5kIHRodXMgYW4gdXBwZXIgYm91bmQgb24gdGhlIGRlbnNpdHksIGxlYWRpbmcgdG8gYSBzbGlnaHRseSBsb3dlciB3ZWlnaHRpbmcgZm9yIHRoZSBzcGF0aWFsIEZEUiAKYWRqdXN0bWVudC4KCmBgYHtyfQojIG5vdyBsb29wIG92ZXIgdGhlc2Ugc3ViLWdyYXBocyB0byBjYWxjdWxhdGUgdGhlIGNvbm5lY3Rpdml0eSAtIHRoaXMgc2VlbXMgYSBsaXR0bGUgc2xvdy4uLgplZGdlLmNvbm5lY3QgPC0gbGFwcGx5KHRlc3Quc3ViZ3JhcGhzLCBGVU49ZnVuY3Rpb24oRUcpIGVkZ2VfY29ubmVjdGl2aXR5KEVHKSkKYGBgCgpUaGUgZWRnZS1jb25uZWN0aXZpdHkgdGFrZXMgfjEwJSBvZiB0aGUgdGltZS4gSG93IGRvIHRoZXkgY29tcGFyZT8KCmBgYHtyfQpwbG90KHVubGlzdCh0ZXN0LmNvbm5lY3QpLCB1bmxpc3QoZWRnZS5jb25uZWN0KSwgbWFpbj0iR3JhcGgtY29ubmVjdGl2aXR5IG1lYXN1cmVzIiwgeGxhYj0iVmVydGV4LWNvbm5lY3Rpdml0eSIsIHlsYWI9IkVkZ2UtY29ubmVjdGl2aXR5IikKYGBgCgpGb3IgdGhlIG1vc3QgcGFydCB0aGUgdmVydGV4LWNvbm5lY3Rpdml0eSBhbmQgZWRnZS1jb25uZWN0aXZpdHkgZm9yIG5laWdoYm9yaG9vZHMgYXJlIGluIHByZXR0eSBnb29kIGFncmVlbWVudC4gSSdsbCBtYXliZSBpbmNsdWRlIHRoZSBvcHRpb24gCnRvIHVzZSBlaXRoZXIgYXMgYSBmdW5jdGlvbiBwYXJhbWV0ZXIuCgpgYGB7cn0KZ3JhcGhfc3BhdGlhbEZEUiA8LSBmdW5jdGlvbihuZWlnaGJvcmhvb2RzLCBncmFwaCwgcHZhbHVlcywgY29ubmVjdGl2aXR5PSd2ZXJ0ZXgnKXsKICAjIGlucHV0IGEgc2V0IG9mIG5laWdoYm9yaG9vZHMgYXMgYSBsaXN0IG9mIGdyYXBoIHZlcnRpY2VzCiAgIyB0aGUgaW5wdXQgZ3JhcGggYW5kIHRoZSB1bmFkanVzdGVkIEdMTSBwLXZhbHVlcwogICMnIG5laWdoYm9yaG9vZHM6IGxpc3Qgb2YgdmVydGljZXMgYW5kIHRoZWlyIHJlc3BlY3RpdmUgbmVpZ2hib3Job29kcwogICMnIGdyYXBoOiBpbnB1dCBrTk4gZ3JhcGgKICAjJyBwdmFsdWVzOiBhIHZlY3RvciBvZiBwdmFsdWVzIGluIHRoZSBzYW1lIG9yZGVyIGFzIHRoZSBuZWlnaGJvcmhvb2QgaW5kaWNlcwogICMnIGNvbm5lY3Rpdml0eTogY2hhcmFjdGVyIC0gZWRnZSBvciB2ZXJ0ZXggdG8gY2FsY3VsYXRlIG5laWdoYm9yaG9vZCBjb25uZWN0aXZpdHkKCiAgIyBEaXNjYXJkaW5nIE5BIHB2YWx1ZXMuCiAgaGFzcHZhbCA8LSAhaXMubmEocHZhbHVlcykKICBpZiAoIWFsbChoYXNwdmFsKSkgewogICAgICBjb29yZHMgPC0gY29vcmRzW2hhc3B2YWwsICwgZHJvcD1GQUxTRV0KICAgICAgcHZhbHVlcyA8LSBwdmFsdWVzW2hhc3B2YWxdCiAgfQogICAgCiAgIyBkZWZpbmUgdGhlIHN1YmdyYXBoIGZvciBlYWNoIG5laWdoYm9yaG9vZCB0aGVuIGNhbGN1bGF0ZSB0aGUgdmVydGV4IGNvbm5lY3Rpdml0eSBmb3IgZWFjaAogICMgdGhpcyBsYXR0ZXIgY29tcHV0YXRpb24gaXMgcXVpdGUgc2xvdyAtIGNhbiBpdCBiZSBzcGVkIHVwPwogIHN1YmdyYXBocyA8LSBsYXBwbHkoMTpsZW5ndGgobmVpZ2hib3Job29kc1toYXNwdmFsXSksCiAgICAgICAgICAgICAgICAgICAgICAgICBGVU49ZnVuY3Rpb24oWCkgaW5kdWNlZF9zdWJncmFwaChncmFwaCwgbmVpZ2hib3Job29kc1toYXNwdmFsXVtbWF1dKSkKCiAgIyBub3cgbG9vcCBvdmVyIHRoZXNlIHN1Yi1ncmFwaHMgdG8gY2FsY3VsYXRlIHRoZSBjb25uZWN0aXZpdHkgLSB0aGlzIHNlZW1zIGEgbGl0dGxlIHNsb3cuLi4KICBpZihjb25uZWN0aXZpdHkgPT0gInZlcnRleCIpewogICAgY29ubmVjdCA8LSBsYXBwbHkoc3ViZ3JhcGhzLCBGVU49ZnVuY3Rpb24oRUcpIHZlcnRleF9jb25uZWN0aXZpdHkoRUcpKQogIH0gZWxzZSBpZihjb25uZWN0aXZpdHkgPT0gImVkZ2UiKXsKICAgIGNvbm5lY3QgPC0gbGFwcGx5KHN1YmdyYXBocywgRlVOPWZ1bmN0aW9uKEVHKSBlZGdlX2Nvbm5lY3Rpdml0eShFRykpCiAgfSBlbHNlewogICAgc3RvcCgiY29ubmVjdGl2aXR5IG9wdGlvbiBub3QgcmVjb2duaXNlZCAtIG11c3QgYmUgZWl0aGVyIGVkZ2Ugb3IgdmVydGV4IikKICB9CiAgCiAgIyB1c2UgMS9jb25uZWN0aXZpdHkgYXMgdGhlIHdlaWdodGluZyBmb3IgdGhlIHdlaWdodGVkIEJIIGFkanVzdG1lbnQgZnJvbSBDeWRhcgogIHcgPC0gMS91bmxpc3QoY29ubmVjdCkKICAjIHNldCBJbmYgdG8gMAogIHdbaXMuaW5maW5pdGUodyldIDwtIDAKICAKICAjIENvbXB1dGluZyBhIGRlbnNpdHktd2VpZ2h0ZWQgcS12YWx1ZS4KICBvIDwtIG9yZGVyKHB2YWx1ZXMpCiAgcHZhbHVlcyA8LSBwdmFsdWVzW29dCiAgdyA8LSB3W29dCgogIGFkanAgPC0gbnVtZXJpYyhsZW5ndGgobykpCiAgYWRqcFtvXSA8LSByZXYoY3VtbWluKHJldihzdW0odykqcHZhbHVlcy9jdW1zdW0odykpKSkKICBhZGpwIDwtIHBtaW4oYWRqcCwgMSkKCiAgaWYgKCFhbGwoaGFzcHZhbCkpIHsKICAgIHJlZnAgPC0gcmVwKE5BX3JlYWxfLCBsZW5ndGgoaGFzcHZhbCkpCiAgICByZWZwW2hhc3B2YWxdIDwtIGFkanAKICAgIGFkanAgPC0gcmVmcAogICAgfQogIHJldHVybihhZGpwKQp9CmBgYAoKCmBgYHtyfQpzdGFydC50aW1lIDwtIFN5cy50aW1lKCkKc2ltMS5zcGF0aWFsZmRyIDwtIGdyYXBoX3NwYXRpYWxGRFIobmVpZ2hib3Job29kcz12ZXJ0ZXgubGlzdCwgZ3JhcGg9c2ltMS5rbm4sIGNvbm5lY3Rpdml0eT0iZWRnZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHB2YWx1ZXM9c2ltMS5yZXNbb3JkZXIoc2ltMS5yZXMkTmVpZ2hib3VyaG9vZCksIF0kUFZhbHVlKQplbmQudGltZSA8LSBTeXMudGltZSgpCmNvbm5lY3QudGltZSA8LSBlbmQudGltZSAtIHN0YXJ0LnRpbWUKc2ltMS5yZXMkU3BhdGlhbEZEUltvcmRlcihzaW0xLnJlcyROZWlnaGJvdXJob29kKV0gPC0gc2ltMS5zcGF0aWFsZmRyCnF2YWxzIDwtIHNpbTEuc3BhdGlhbGZkcgppcy5zaWcgPC0gcXZhbHMgPD0gMC4wMQpzdW1tYXJ5KGlzLnNpZykKYGBgCgpIb3cgZG8gdGhlc2UgY29tcGFyZSB0byB0aGUgc3RhbmRhcmQgRkRSIGFkanVzdG1lbnQ/CgpgYGB7cn0KcGxvdCgtbG9nMTAoc2ltMS5yZXNbb3JkZXIoc2ltMS5yZXMkTmVpZ2hib3VyaG9vZCksIF0kRkRSKSwKICAgICAtbG9nMTAoc2ltMS5zcGF0aWFsZmRyKSwgbWFpbj0iRkRSIGNvbXBhcmlzb24iLCB4bGFiPSItbG9nMTAgRkRSIiwgeWxhYj0iLWxvZzEwIFNwYXRpYWwgRkRSIikKYGBgCgpJbiB0aGlzIGluc3RhbmNlIHRoZXJlIGlzIG5vdCBhIGh1Z2UgZGlmZmVyZW5jZS4gVGhhdCBzZWVtcyB0byB3b3JrIGZhaXJseSB3ZWxsLiBEb2VzIGl0IG1ha2Ugc2Vuc2UgdGhvdWdoPyBpLmUuIGFyZSB0aGUgY2hhbmdlcyBpbiB0aGUgZ3JhcGggaW4gdGhlIApleHBlY3RlZCByZWdpb25zPyBJJ2xsIGZvbGxvdyB0aGUgYEN5ZGFyYCB3b3JrZmxvdyBmb3IgdGhpcyBhbmQgcHJvamVjdCB0aGUgbmVpZ2hib3VyaG9vZHMgaW50byBhIHJlZHVjZWQgZGltZW5zaW9uYWwgc3BhY2UuCgpgYGB7cn0KbmVpZ2hib3Job29kX2V4cHJlc3Npb24gPC0gZnVuY3Rpb24obmVpZ2hib3Job29kcywgZGF0YS5zZXQpewogICMgSSdsbCBjYWxjdWxhdGUgdGhlIGF2ZXJhZ2UgdmFsdWUgb2YgZWFjaCBuZWlnaGJvcmhvb2QgZm9yIGVhY2ggb2YgdGhlIG4gZmVhdHVyZXMgaW4gdGhlIGRhdGEuc2V0CiAgbmVpZ2hib3VyLm1vZGVsIDwtIG1hdHJpeCgwTCwgbmNvbD1sZW5ndGgobmVpZ2hib3Job29kcyksIG5yb3c9bmNvbChkYXRhLnNldCkpCiAgIyBuZWlnaGJvdXIubW9kZWwgPC0gc2FwcGx5KDE6bGVuZ3RoKG5laWdoYm9yaG9vZHMpLCBGVU49ZnVuY3Rpb24oWCkgcHJpbnQobGVuZ3RoKG5laWdoYm91ci5tb2RlbFssIFhdKSkpCiAgZm9yKHggaW4gc2VxX2Fsb25nKDE6bGVuZ3RoKG5laWdoYm9yaG9vZHMpKSl7CiAgICBuZWlnaGJvdXIubW9kZWxbbmVpZ2hib3Job29kc1tbeF1dLCB4XSA8LSAxCiAgfQogIAogIG5laWdoLmV4cHJzIDwtIHQobmVpZ2hib3VyLm1vZGVsKSAlKiUgdChkYXRhLnNldCkKICBuZWlnaC5leHBycyA8LSB0KGFwcGx5KG5laWdoLmV4cHJzLCAyLCBGVU49ZnVuY3Rpb24oWFApIFhQL2NvbFN1bXMobmVpZ2hib3VyLm1vZGVsKSkpCgogIHJldHVybihuZWlnaC5leHBycykKfQpgYGAKCgpgYGB7cn0Kc2ltMS5uZWlnaGJvdXIuZXhwcnMgPC0gbmVpZ2hib3Job29kX2V4cHJlc3Npb24odmVydGV4Lmxpc3QsIHNpbTEuZ2V4KQpgYGAKCkVtYmVkIHRoZXNlIGh5cGVyc3BoZXJlcyB3aXRoIGEgUENBIGFuZCBVTUFQLgoKYGBge3J9CnNpbTEubmVpZ2hib3VyLnBjYSA8LSBwcmNvbXAoKHQoc2ltMS5uZWlnaGJvdXIuZXhwcnMpKSkKcGFpcnMoc2ltMS5uZWlnaGJvdXIucGNhJHhbLCBjKDE6NSldKQpgYGAKCmBgYHtyfQpzZXQuc2VlZCg0MikKbmVpZ2hib3VyaG9vZC51bWFwIDwtIHVtYXAoc2ltMS5uZWlnaGJvdXIucGNhJHhbLCBjKDE6MzApXSwKICAgICAgICAgICAgICAgICAgICAgbl9jb21wb25lbnRzPTIsCiAgICAgICAgICAgICAgICAgICAgIG5fbmVpZ2hib3JzPTIxLCBtZXRyaWM9J2V1Y2xpZGVhbicsCiAgICAgICAgICAgICAgICAgICAgIGluaXQ9J3JhbmRvbScsIG1pbl9kaXN0PTAuMSkKcGxvdChuZWlnaGJvdXJob29kLnVtYXAkbGF5b3V0LAogICAgIHhsYWI9IlVNQVAgMSIsIHlsYWI9IlVNQVAgMiIpCmBgYAoKV2UgY2FuIG92ZXJsYXkgdGhlIERBIHRlc3Rpbmcgb24gdGhlc2UgbmVpZ2hib3VyaG9vZHMuCgpgYGB7cn0KbmVpZ2hib3IuZGYgPC0gc2ltMS5yZXNbLCBjKCJsb2dGQyIsICJOZWlnaGJvdXJob29kIiwgIlNwYXRpYWxGRFIiKV0KbmVpZ2hib3IuZGYgPC0gZG8uY2FsbChjYmluZC5kYXRhLmZyYW1lLCBsaXN0KG5laWdoYm9yLmRmLCBhcy5kYXRhLmZyYW1lKG5laWdoYm91cmhvb2QudW1hcCRsYXlvdXQpKSkKY29sbmFtZXMobmVpZ2hib3IuZGYpIDwtIGMoImxvZ0ZDIiwgIk5laWdoYm91cmhvb2QiLCAiU3BhdGlhbEZEUiIsICJVTUFQMSIsICJVTUFQMiIpCm5laWdoYm9yLmRmJFNpZyA8LSBhcy5udW1lcmljKG5laWdoYm9yLmRmJFNwYXRpYWxGRFIgPD0gMC4wNSkKCmdncGxvdChuZWlnaGJvci5kZiwgYWVzKHg9VU1BUDEsIHk9VU1BUDIpKSArCiAgZ2VvbV9wb2ludChkYXRhPW5laWdoYm9yLmRmW25laWdoYm9yLmRmJFNpZyA9PSAwLCBdLAogICAgICAgICAgICAgY29sb3VyPSdncmV5ODAnLCBzaXplPTIpICsKICBnZW9tX3BvaW50KGRhdGE9bmVpZ2hib3IuZGZbbmVpZ2hib3IuZGYkU2lnID09IDEsIF0sCiAgICAgICAgICAgICBhZXMoY29sb3VyPWxvZ0ZDKSwgc2l6ZT00KSArCiAgdGhlbWVfY2xlYW4oKSArCiAgc2NhbGVfY29sb3VyX2dyYWRpZW50Mihsb3c9ImJsdWUiLCBtaWQ9ImdyZXk4MCIsIGhpZ2g9InJlZCIpCmBgYAoKSW4gZWFjaCBuZWlnaGJvdXJob29kLCB3aGF0IGlzIHRoZSBtb3N0IGNvbW1vbiBjb25kaXRpb24gb3IgYmxvY2sgb2YgY2VsbHM/CgpgYGB7cn0KbmVpZ2hib3VyLmxpc3QgPC0gbGlzdCgpCmZvcih4IGluIHNlcV9hbG9uZygxOmxlbmd0aCh2ZXJ0ZXgubGlzdCkpKXsKICB4LmRmIDwtIG1ldGEuZGZbbWV0YS5kZiRWZXJ0ZXggJWluJSB2ZXJ0ZXgubGlzdFtbeF1dLCBdCiAgeC5yZXAgPC0gbmFtZXModGFibGUoeC5kZiRSZXBsaWNhdGUpKVt3aGljaCh0YWJsZSh4LmRmJFJlcGxpY2F0ZSkgPT0gbWF4KHRhYmxlKHguZGYkUmVwbGljYXRlKSkpXQogIGlmKGxlbmd0aCh4LnJlcCkgPiAxKXsKICAgIHgucmVwIDwtIHNhbXBsZShzaXplPTEsIHgucmVwKQogIH0KICB4LmJsb2NrIDwtIG5hbWVzKHRhYmxlKHguZGYkQmxvY2spKVt3aGljaCh0YWJsZSh4LmRmJEJsb2NrKSA9PSBtYXgodGFibGUoeC5kZiRCbG9jaykpKV0KICAgIGlmKGxlbmd0aCh4LmJsb2NrKSA+IDEpewogICAgeC5ibG9jayA8LSBzYW1wbGUoc2l6ZT0xLCB4LmJsb2NrKQogIH0KICB4LmNvbmRpdGlvbiA8LSBuYW1lcyh0YWJsZSh4LmRmJENvbmRpdGlvbikpW3doaWNoKHRhYmxlKHguZGYkQ29uZGl0aW9uKSA9PSBtYXgodGFibGUoeC5kZiRDb25kaXRpb24pKSldCiAgICBpZihsZW5ndGgoeC5jb25kaXRpb24pID4gMSl7CiAgICB4LmNvbmRpdGlvbiA8LSBzYW1wbGUoc2l6ZT0xLCB4LmNvbmRpdGlvbikKICB9CiAgCiAgbmVpZ2hib3VyLmxpc3RbW3hdXSA8LSBkYXRhLmZyYW1lKCJSZXBsaWNhdGUiPXgucmVwLCAiQmxvY2siPXguYmxvY2ssICJDb25kaXRpb24iPXguY29uZGl0aW9uLCAiTmVpZ2hib3VyaG9vZCI9eCkKfQoKbmVpZ2hib3VyLm1ldGEgPC0gZG8uY2FsbChyYmluZC5kYXRhLmZyYW1lLCBuZWlnaGJvdXIubGlzdCkKbmVpZ2hib3VyLm1lcmdlIDwtIG1lcmdlKG5laWdoYm9yLmRmLCBuZWlnaGJvdXIubWV0YSwgYnk9J05laWdoYm91cmhvb2QnKQpuZWlnaGJvdXIubWVyZ2UkQmxvY2sgPC0gb3JkZXJlZChuZWlnaGJvdXIubWVyZ2UkQmxvY2ssCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxldmVscz1jKCJCMSIsICJCMiIsICJCMyIpKQpuZWlnaGJvdXIubWVyZ2UkRGlmZiA8LSBzaWduKG5laWdoYm91ci5tZXJnZSRsb2dGQykKbmVpZ2hib3VyLm1lcmdlJERpZmZbbmVpZ2hib3VyLm1lcmdlJFNpZyA9PSAwXSA8LSAwCmBgYAoKCmBgYHtyLCBmaWcud2lkdGg9OS43NSwgZmlnLmhlaWdodD00LjE1fQpnZ3Bsb3QobmVpZ2hib3VyLm1lcmdlLCBhZXMoeD1VTUFQMSwgeT1VTUFQMikpICsKICBnZW9tX3BvaW50KGRhdGE9bmVpZ2hib3VyLm1lcmdlWywgYygiVU1BUDEiLCAiVU1BUDIiKV0sCiAgICAgICAgICAgICBjb2xvdXI9J2dyZXk4MCcsIHNpemU9MSkgKwogIGdlb21fcG9pbnQoZGF0YT1uZWlnaGJvdXIubWVyZ2VbbmVpZ2hib3VyLm1lcmdlJFNpZyA9PSAxLCBdLAogICAgICAgICAgICAgYWVzKGNvbG91cj1sb2dGQyksIHNpemU9NCkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9ncmFkaWVudDIobG93PSJibHVlIiwgbWlkPSJncmV5ODAiLCBoaWdoPSJyZWQiKSArCiAgZmFjZXRfd3JhcCh+QmxvY2spCmBgYAoKVGhpcyBkb2Vzbid0IG1ha2Ugc2Vuc2UgLSBCbG9jayAzIHNob3VsZG4ndCBoYXZlIGFueSBEQSBuZWlnaGJvdXJob29kcy4gSXMgdGhpcyBhIGNvbXBvc2l0aW9uYWwgZWZmZWN0IHdlJ3JlIHNlZWluZyBoZXJlPyBJdCdzIHN0cmFuZ2UgCnRoYXQgYSByYW5kb20gZmx1Y3R1YXRpb24gd291bGQgY2F1c2UgdGhpcyAtIGl0IG11c3QgYmUgaW5jcmVkaWJseSBzZW5zaXRpdmUuCgpgYGB7cn0KdGFibGUobmVpZ2hib3VyLm1lcmdlJEJsb2NrLCBuZWlnaGJvdXIubWVyZ2UkRGlmZikKYGBgCgpJcyB0aGlzIGR1ZSB0byBzb21lIHdlaXJkIGdsb2JhbCBzY2FsaW5nIGRpZmZlcmVuY2VzIG9yIGlzIHRoaXMgYSBjb21wb3NpdGlvbmFsIGVmZmVjdD8gSW4gYWRkaXRpb24gdG8gdGhlIGhpZ2ggZmFsc2UtcG9zaXRpdmUgcmF0ZSwgdGhlcmUgYXJlIAphbHNvIHNpZ24gZGlmZmVyZW5jZXMgd2hlcmUgdGhleSBzaG91bGQgYmUgY29uY29yZGFudCBfd2l0aGluXyBhIGJsb2NrIG9mIGRhdGEgcG9pbnRzLiBGb3IgZWFjaCBuZWlnaGJvdXJob29kLCBob3cgbXVjaCBkb2VzIGl0IG92ZXJsYXAgd2l0aCB0aGUgCm92ZXIgYmxvY2tzPyBJbnR1aXRpdmVseSBJIHdvdWxkIGhhdmUgZXhwZWN0ZWQgMCBhcyB0aGV5IGFyZSB3ZWxsIHNlcGFyYXRlZCwgaG93ZXZlciwgdGhpcyBtaWdodCBiZSBhIGNhdXNlIGZvciBhbGwgdGhlc2UgZmFsc2UgREEgbmVpZ2hib3VyaG9vZHMuCgpfX05CX186IFRoaXMgd2FzIGR1ZSB0byBkaWZmZXJlbnQgdmVydGljZXMgYmVpbmcgc2FtcGxlZCwgc28gdGhlIHJlc3VsdHMgd2VyZW4ndCBjb25jb3JkYW50IC0gc2V0IHRoZSBzYW1lIHNlZWQgTWlrZSEhISEKClRoZSBmYWxzZS1wb3NpdGl2ZSByYXRlIGlzIGEgbGl0dGxlIGhpZ2ggaGVyZSwgYnV0IGF0IGxlYXN0IHRoZSBzaWducyBhcmUgdGhlIGNvcnJlY3Qgd2F5IGFyb3VuZC4KCmBgYHtyfQpwbG90KHg9c2ltMS5yZXMkbG9nRkMsIHk9LWxvZzEwKHNpbTEucmVzJFNwYXRpYWxGRFIpLCB4bGFiPSJsb2cgRkMiLCB5bGFiPSJTcGF0aWFsIEZEUiIsCiAgICAgY29sPWlmZWxzZShzaW0xLnJlcyRTaWcgPT0gMSwgInJlZCIsICJibGFjayIpKQpgYGAKCkZvciBlYWNoIG5laWdoYm91cmhvb2QsIEkgbmVlZCB0byBjb3VudCB0aGUgbnVtYmVyIG9mIHBvaW50cyBpbiBlYWNoIGJsb2NrLCBhcyB3ZWxsIGFzIGNvbmRpdGlvbiBhbmQgcmVwbGljYXRlLgoKYGBge3IsIGZpZy5oZWlnaHQ9OC45NSwgZmlnLndpZHRoPTkuOTV9CmFsbC5zYW1wcyA8LSB1bmlxdWUocGFzdGUobWV0YS5kZiRCbG9jaywgbWV0YS5kZiRDb25kaXRpb24sIG1ldGEuZGYkUmVwbGljYXRlLCBzZXA9Il8iKSkKbWV0YS5kZiRBbGwuU2FtcGxlIDwtIHBhc3RlKG1ldGEuZGYkQmxvY2ssIG1ldGEuZGYkQ29uZGl0aW9uLCBtZXRhLmRmJFJlcGxpY2F0ZSwgc2VwPSJfIikKYWxsLmNvdW50Lm1hdHJpeCA8LSBtYXRyaXgoMEwsIG5jb2w9bGVuZ3RoKGFsbC5zYW1wcyksIG5yb3c9bGVuZ3RoKHZlcnRleC5saXN0KSkKY29sbmFtZXMoYWxsLmNvdW50Lm1hdHJpeCkgPC0gYWxsLnNhbXBzCiAgCmZvcih4IGluIHNlcV9hbG9uZygxOmxlbmd0aCh2ZXJ0ZXgubGlzdCkpKXsKICB2LnggPC0gdmVydGV4Lmxpc3RbW3hdXQogIGZvcihpIGluIHNlcV9hbG9uZygxOmxlbmd0aChhbGwuc2FtcHMpKSl7CiAgICBpLnMgPC0gYWxsLnNhbXBzW2ldCiAgICBpLnMudmVydGljZXMgPC0gaW50ZXJzZWN0KHYueCwgbWV0YS5kZlttZXRhLmRmJEFsbC5TYW1wbGUgPT0gaS5zLCBdJFZlcnRleCkKICAgIGFsbC5jb3VudC5tYXRyaXhbeCwgaV0gPC0gbGVuZ3RoKGkucy52ZXJ0aWNlcykKICB9Cn0KCmFsbC5jb3VudC5tZWx0IDwtIG1lbHQoYWxsLmNvdW50Lm1hdHJpeCkKYWxsLmNvdW50Lm1lbHQkVmFyMiA8LSBhcy5jaGFyYWN0ZXIoYWxsLmNvdW50Lm1lbHQkVmFyMikKYWxsLmNvdW50Lm1lbHQkQmxvY2sgPC0gdW5saXN0KGxhcHBseShzdHJzcGxpdChhbGwuY291bnQubWVsdCRWYXIyLCBzcGxpdD0iXyIsIGZpeGVkPVRSVUUpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEZVTj1mdW5jdGlvbihYUCkgcGFzdGUwKFhQWzFdKSkpCmFsbC5jb3VudC5tZWx0JENvbmRpdGlvbiA8LSB1bmxpc3QobGFwcGx5KHN0cnNwbGl0KGFsbC5jb3VudC5tZWx0JFZhcjIsIHNwbGl0PSJfIiwgZml4ZWQ9VFJVRSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRlVOPWZ1bmN0aW9uKFhQKSBwYXN0ZTAoWFBbMl0pKSkKYWxsLmNvdW50Lm1lbHQkUmVwbGljYXRlIDwtIHVubGlzdChsYXBwbHkoc3Ryc3BsaXQoYWxsLmNvdW50Lm1lbHQkVmFyMiwgc3BsaXQ9Il8iLCBmaXhlZD1UUlVFKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBGVU49ZnVuY3Rpb24oWFApIHBhc3RlMChYUFszXSkpKQoKZ2dwbG90KGFsbC5jb3VudC5tZWx0LCBhZXMoeD1CbG9jaywgeT12YWx1ZSwgZmlsbD1Db25kaXRpb24pKSArCiAgZ2VvbV9ib3hwbG90KCkgKwogIHRoZW1lX2NsZWFuKCkgKwogIGZhY2V0X3dyYXAoflZhcjEsIHNjYWxlcz0iZnJlZV95IikKYGBgCgpFYWNoIHBhbmVsIGlzIGEgbmVpZ2hib3VyaG9vZCwgdGhlIG51bWJlcnMgdGhlIGNvdW50cyBvZiBkYXRhIHBvaW50cyBpbiBlYWNoIHRoYXQgY29tIGZyb20gZWl0aGVyIGNvbmRpdGlvbiwgYW5kIGluIHRoZSBibG9jayBvZiBwb2ludHMuIFRoZXNlIAphcmUgbm93IGNvbmNvcmRhbnQgd2l0aCB0aGUgREEgcmVzdWx0cywgZXhjZXB0IG5laWdoYm91cmhvb2RzIDcgYW5kIDI2IC0gd2hpY2ggbG9vayBsaWtlIGEgY29tYmluYXRpb24gb2Ygc2FtcGxpbmcgdmFyaWFuY2UsIHdoaWNoIHRoZW4gCmNyZWF0ZXMgYSBjb21wb3NpdGlvbmFsIGVmZmVjdC4gRWl0aGVyIGEgZmlsdGVyIG9uIGxvdy1jb3VudCBuZWlnaGJvdXJob29kcyBvciBvbiB0aGUgbG9nIGZvbGQgY2hhbmdlcyB3b3VsZCBhbWVsaW9yYXRlIHRoaXMuCgpfX05CX186IEVtbWEgc3VnZ2VzdGVkIHVzaW5nIHRoZSB3aXRoaW4gbmVpZ2hib3VyaG9vZCBkaXN0YW5jZXMgaW5zdGVhZCBvZiBjb25uZWN0aXZpdHkgYXMgYSBtZWFzdXJlIG9mIGRlbnNpdHkuIFRoaXMgd291bGQgcmVxdWlyZSBtYWtpbmcgb3VyIApvd24gS05OLWdyYXBoIGJ1aWxkaW5nIGZ1bmN0aW9uIHRoYXQgcmV0YWlucyB0aGUgZGlzdGFuY2VzIGJldHdlZW4gYWxsIHBhaXJzIG9mIHZlcnRpY2VzLiBJbiB0cnV0aCwgdGhpcyBjb3VsZCBqdXN0IGJlIGFkZGVkIGFzIHRoZSBlZGdlIHdlaWdodHMgCnRvIHRoZSBLTk4tZ3JhcGgsIHdoaWNoIGFyZSB1bHRpbWF0ZWx5IGlnbm9yZWQgZm9yIHRoZSBwdXJwb3NlIG9mIGdyYXBoIGJ1aWxkaW5nLCBpLmUuIGFsbCBlZGdlcyA9IDEuCgojIyMgVXNpbmcgZGlzdGFuY2UgYmV0d2VlbiB2ZXJ0aWNlcyBmb3Igc3BhdGlhbCBGRFIgY29ycmVjdGlvbi4KCldlIGhhdmUgdGhlIFBDIGNvb3JkaW5hdGVzIHRoYXQgd2VyZSB1c2VkIGluIHRoZSBvcmlnaW5hbCBLTk4gZ3JhcGggYnVpbGRpbmcsIHNvIHN0cmljdGx5IHdlIGp1c3QgbmVlZCB0aGlzIHdpdGggdGhlIG5laWdoYm91cmhvb2QgbGlzdCB0byAKY2FsY3VsYXRlIHRoZSBFdWNsaWRlYW4gZGlzdGFuY2VzLiBGb3IgdGhlIG5laWdoYm91cmhvb2QsIGRvIHdlIHRha2UgdGhlIGF2ZXJhZ2Ugb2ZmLWRpYWdvbmFsIGRpc3RhbmNlIGFzIHRoZSBkZW5zaXR5PwoKYGBge3J9Cm5laWdoYm91ci5kaXN0Lmxpc3QgPC0gbGlzdCgpCmZvcih4IGluIHNlcV9hbG9uZygxOmxlbmd0aCh2ZXJ0ZXgubGlzdCkpKXsKICB4LnZlcnRzIDwtIHZlcnRleC5saXN0W1t4XV0KICB4LnBjcyA8LSBzaW0xLnBjYSR4W3gudmVydHMsIGMoMTozMCldCiAgeC5ldWNsaWQgPC0gYXMubWF0cml4KGRpc3QoeC5wY3MpKQogIHguZGlzdGRlbnMgPC0gMS9tZWFuKHguZXVjbGlkW2xvd2VyLnRyaSh4LmV1Y2xpZCwgZGlhZz1GQUxTRSldKQogIG5laWdoYm91ci5kaXN0Lmxpc3RbW3hdXSA8LSB4LmRpc3RkZW5zCn0KCmhpc3QodW5saXN0KG5laWdoYm91ci5kaXN0Lmxpc3QpLCAxMDAsIHhsYWI9IjEvbWVhbiBFdWNsaWRlYW4gZGlzdGFuY2UiKQpgYGAKClRoaXMgbG9va3MgYSBsaXR0bGUgbGlrZSB0aGUgMyBibG9ja3Mgb2YgcG9pbnRzLi4uIExldCdzIHRyeSB0aGlzIGZvciB3ZWlnaHRpbmcgdGhlIHNwYXRpYWwgRkRSLgoKYGBge3J9CmdyYXBoX3NwYXRpYWxGRFIgPC0gZnVuY3Rpb24obmVpZ2hib3Job29kcywgZ3JhcGgsIHB2YWx1ZXMsIGNvbm5lY3Rpdml0eT0ndmVydGV4JywgcGNhPU5VTEwpewogICMgaW5wdXQgYSBzZXQgb2YgbmVpZ2hib3Job29kcyBhcyBhIGxpc3Qgb2YgZ3JhcGggdmVydGljZXMKICAjIHRoZSBpbnB1dCBncmFwaCBhbmQgdGhlIHVuYWRqdXN0ZWQgR0xNIHAtdmFsdWVzCiAgIycgbmVpZ2hib3Job29kczogbGlzdCBvZiB2ZXJ0aWNlcyBhbmQgdGhlaXIgcmVzcGVjdGl2ZSBuZWlnaGJvcmhvb2RzCiAgIycgZ3JhcGg6IGlucHV0IGtOTiBncmFwaAogICMnIHB2YWx1ZXM6IGEgdmVjdG9yIG9mIHB2YWx1ZXMgaW4gdGhlIHNhbWUgb3JkZXIgYXMgdGhlIG5laWdoYm9yaG9vZCBpbmRpY2VzCiAgIycgY29ubmVjdGl2aXR5OiBjaGFyYWN0ZXIgLSBlZGdlIG9yIHZlcnRleCB0byBjYWxjdWxhdGUgbmVpZ2hib3Job29kIGNvbm5lY3Rpdml0eSBvciBkaXN0YW5jZSB0byB1c2UgYXZlcmFnZSBFdWNsaWRlYW4gZGlzdGFuY2UKICAjJyBwY2E6IG1hdHJpeCBvZiBQQ3MgdG8gY2FsY3VsYXRlIEV1Y2xpZGVhbiBkaXN0YW5jZXMsIG9ubHkgcmVxdWlyZWQgd2hlbiBjb25uZWN0aXZpdHkgPT0gZGlzdGFuY2UKCiAgIyBEaXNjYXJkaW5nIE5BIHB2YWx1ZXMuCiAgaGFzcHZhbCA8LSAhaXMubmEocHZhbHVlcykKICBpZiAoIWFsbChoYXNwdmFsKSkgewogICAgICBjb29yZHMgPC0gY29vcmRzW2hhc3B2YWwsICwgZHJvcD1GQUxTRV0KICAgICAgcHZhbHVlcyA8LSBwdmFsdWVzW2hhc3B2YWxdCiAgfQogICAgCiAgIyBkZWZpbmUgdGhlIHN1YmdyYXBoIGZvciBlYWNoIG5laWdoYm9yaG9vZCB0aGVuIGNhbGN1bGF0ZSB0aGUgdmVydGV4IGNvbm5lY3Rpdml0eSBmb3IgZWFjaAogICMgdGhpcyBsYXR0ZXIgY29tcHV0YXRpb24gaXMgcXVpdGUgc2xvdyAtIGNhbiBpdCBiZSBzcGVkIHVwPwogIHN1YmdyYXBocyA8LSBsYXBwbHkoMTpsZW5ndGgobmVpZ2hib3Job29kc1toYXNwdmFsXSksCiAgICAgICAgICAgICAgICAgICAgICAgICBGVU49ZnVuY3Rpb24oWCkgaW5kdWNlZF9zdWJncmFwaChncmFwaCwgbmVpZ2hib3Job29kc1toYXNwdmFsXVtbWF1dKSkKCiAgIyBub3cgbG9vcCBvdmVyIHRoZXNlIHN1Yi1ncmFwaHMgdG8gY2FsY3VsYXRlIHRoZSBjb25uZWN0aXZpdHkgLSB0aGlzIHNlZW1zIGEgbGl0dGxlIHNsb3cuLi4KICBpZihjb25uZWN0aXZpdHkgPT0gInZlcnRleCIpewogICAgdC5jb25uZWN0IDwtIGxhcHBseShzdWJncmFwaHMsIEZVTj1mdW5jdGlvbihFRykgdmVydGV4X2Nvbm5lY3Rpdml0eShFRykpCiAgfSBlbHNlIGlmKGNvbm5lY3Rpdml0eSA9PSAiZWRnZSIpewogICAgdC5jb25uZWN0IDwtIGxhcHBseShzdWJncmFwaHMsIEZVTj1mdW5jdGlvbihFRykgZWRnZV9jb25uZWN0aXZpdHkoRUcpKQogIH0gZWxzZSBpZihjb25uZWN0aXZpdHkgPT0gImRpc3RhbmNlIil7CiAgICBpZighaXMubnVsbChwY2EpKXsKICAgICAgdC5jb25uZWN0IDwtIGxhcHBseSgxOmxlbmd0aChuZWlnaGJvcmhvb2RzW2hhc3B2YWxdKSwKICAgICAgICAgICAgICAgICAgICAgICAgRlVOPWZ1bmN0aW9uKFBHKSB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgeC5wY3MgPC0gcGNhW25laWdoYm9yaG9vZHNbaGFzcHZhbF1bW1BHXV0sIF0KICAgICAgICAgICAgICAgICAgICAgICAgICB4LmV1Y2xpZCA8LSBhcy5tYXRyaXgoZGlzdCh4LnBjcykpCiAgICAgICAgICAgICAgICAgICAgICAgICAgeC5kaXN0ZGVucyA8LSAxL21lYW4oeC5ldWNsaWRbbG93ZXIudHJpKHguZXVjbGlkLCBkaWFnPUZBTFNFKV0pCiAgICAgICAgICAgICAgICAgICAgICAgIHJldHVybih4LmRpc3RkZW5zKX0pCiAgICB9IGVsc2V7CiAgICAgIHN0b3AoIkEgbWF0cml4IG9mIFBDcyBpcyByZXF1aXJlZCB0byBjYWxjdWxhdGUgZGlzdGFuY2VzIikgIAogICAgfQogIH1lbHNlewogICAgc3RvcCgiY29ubmVjdGl2aXR5IG9wdGlvbiBub3QgcmVjb2duaXNlZCAtIG11c3QgYmUgZWl0aGVyIGVkZ2UsIHZlcnRleCBvciBkaXN0YW5jZSIpCiAgfQogIAogICMgdXNlIDEvY29ubmVjdGl2aXR5IGFzIHRoZSB3ZWlnaHRpbmcgZm9yIHRoZSB3ZWlnaHRlZCBCSCBhZGp1c3RtZW50IGZyb20gQ3lkYXIKICB3IDwtIDEvdW5saXN0KHQuY29ubmVjdCkKICB3W2lzLmluZmluaXRlKHcpXSA8LSAwCiAgCiAgIyBDb21wdXRpbmcgYSBkZW5zaXR5LXdlaWdodGVkIHEtdmFsdWUuCiAgbyA8LSBvcmRlcihwdmFsdWVzKQogIHB2YWx1ZXMgPC0gcHZhbHVlc1tvXQogIHcgPC0gd1tvXQoKICBhZGpwIDwtIG51bWVyaWMobGVuZ3RoKG8pKQogIGFkanBbb10gPC0gcmV2KGN1bW1pbihyZXYoc3VtKHcpKnB2YWx1ZXMvY3Vtc3VtKHcpKSkpCiAgYWRqcCA8LSBwbWluKGFkanAsIDEpCgogIGlmICghYWxsKGhhc3B2YWwpKSB7CiAgICByZWZwIDwtIHJlcChOQV9yZWFsXywgbGVuZ3RoKGhhc3B2YWwpKQogICAgcmVmcFtoYXNwdmFsXSA8LSBhZGpwCiAgICBhZGpwIDwtIHJlZnAKICAgIH0KICByZXR1cm4oYWRqcCkKfQpgYGAKCgpgYGB7cn0Kc3RhcnQudGltZSA8LSBTeXMudGltZSgpCnNpbTEuc3BhdGlhbGZkciA8LSBncmFwaF9zcGF0aWFsRkRSKG5laWdoYm9yaG9vZHM9dmVydGV4Lmxpc3QsIGdyYXBoPXNpbTEua25uLCBjb25uZWN0aXZpdHk9ImRpc3RhbmNlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGNhPXNpbTEucGNhJHhbLCBjKDE6MzApXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHZhbHVlcz1zaW0xLnJlc1tvcmRlcihzaW0xLnJlcyROZWlnaGJvdXJob29kKSwgXSRQVmFsdWUpCmVuZC50aW1lIDwtIFN5cy50aW1lKCkKZGlzdGFuY2UudGltZSA8LSBlbmQudGltZSAtIHN0YXJ0LnRpbWUKc2ltMS5yZXMkU3BhdGlhbEZEUi5EaXN0W29yZGVyKHNpbTEucmVzJE5laWdoYm91cmhvb2QpXSA8LSBzaW0xLnNwYXRpYWxmZHIKcXZhbHMgPC0gc2ltMS5zcGF0aWFsZmRyCmlzLnNpZyA8LSBxdmFscyA8PSAwLjAxCnN1bW1hcnkoaXMuc2lnKQpgYGAKCkhvdyBkb2VzIHRoaXMgY29tcGFyZSB3aXRoIHVzaW5nIGNvbm5lY3Rpdml0eSBmb3IgdGhlIHNwYXRpYWwgRkRSIGNvcnJlY3Rpb24/CgpgYGB7cn0Kc2luayhmaWxlPSIvZGV2L251bGwiKQpwZGYoIn4vRHJvcGJveC9NaWxvL3NpbXVsYXRpb25zL0Nvbm5lY3Rpdml0eV92c19kaXN0YW5jZV9TcGF0aWFsRkRSLnBkZiIsCiAgICBoZWlnaD0zLjE1LCB3aWR0aD00LjI1LCB1c2VEaW5nYmF0cz1GQUxTRSkKcGxvdCgtbG9nMTAoc2ltMS5yZXMkU3BhdGlhbEZEUi5EaXN0KSwgLWxvZzEwKHNpbTEucmVzJFNwYXRpYWxGRFIpLCB4bGFiPSItbG9nMTAgZGlzdGFuY2UgU3BhdGlhbCBGRFIiLAogICAgIHlsYWI9Ii1sb2cxMCBjb25uZWN0aXZpdHkgU3BhdGlhbCBGRFIiKQpkZXYub2ZmKCkKc2luayhmaWxlPU5VTEwpCgpwbG90KC1sb2cxMChzaW0xLnJlcyRTcGF0aWFsRkRSLkRpc3QpLCAtbG9nMTAoc2ltMS5yZXMkU3BhdGlhbEZEUiksIHhsYWI9Ii1sb2cxMCBkaXN0YW5jZSBTcGF0aWFsIEZEUiIsCiAgICAgeWxhYj0iLWxvZzEwIGNvbm5lY3Rpdml0eSBTcGF0aWFsIEZEUiIpCmBgYAoKQXNpZGUgZnJvbSB0aGUgc21hbGwgbnVtYmVyIG9mIGZhbHNlLXBvc2l0aXZlIHNhbXBsZXMsIHRoaXMgd29ya3MgZmFpcmx5IHdlbGwgb24gdGhlc2Ugc2ltdWxhdGVkIGRhdGEuIENhbiB3ZSBhbHNvIGdldCBzaW1pbGFyIHJlc3VsdHMgd2l0aCBhIApyZWFsLXdvcmxkIGRhdGE/IEZvciB0aGlzICdsbCB0ZXN0IHRoZSAxIGFuZCA1MiB3ZWVrIG9sZCBURUMgZnJvbSBvdXIgQWdlaW5nIHRoeW11cyBwYXBlciwgYXMgdGhlc2UgaGF2ZSB0aGUgYmlnZ2VzdCBkaWZmZXJlbmNlcyBiZXR3ZWVuIHRoZW0uCgojIyBBZ2VpbmcgVEVDCgpgYGB7cn0KIyByZWFkIGluIG5vcm1hbGlzZWQgZGF0YSwgc3Vic2V0IHRvIDEgYW5kIDUyIHdlZWsgb2xkIFRFQywgZG8gYSBQQ0EgYW5kIGNvbnN0cnVjdCBhIGtOTiBncmFwaC4KdGVjLm1ldGEgPC0gcmVhZC50YWJsZSgifi9Ecm9wYm94L0FnZWluZ0V4cGVyaW1lbnQvRnJvemVuL1RoeW11c01hcmtlcl90U05FX1BDQV9tZXRhLnRzdiIsCiAgICAgICAgICAgICAgICAgICAgICAgc2VwPSJcdCIsIGhlYWRlcj1UUlVFLCBzdHJpbmdzQXNGYWN0b3JzPUZBTFNFKQojIGV4Y2x1ZGUgdGVjaG5pY2FsIGFydGlmYWN0IGNsdXN0ZXIKdGVjLm1ldGEgPC0gdGVjLm1ldGFbIXRlYy5tZXRhJFRGSURGLkNsdXN0ZXIgJWluJSBjKDQpLCBdCnRlYy5zdWIubWV0YSA8LSB0ZWMubWV0YVt0ZWMubWV0YSRBZ2UgJWluJSBjKCIxd2siLCAiNTJ3ayIpLCBdCgojIGFkZCB0aGUgbGFiZWwgYW5ub3RhdGlvbgp0ZWMuc3ViLm1ldGEkQ2x1c3RlciA8LSAiVW5rbm93biIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjIiXSA8LSAiSW50ZXJ0eXBpY2FsIFRFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjkiXSA8LSAiUGVyaW5hdGFsIGNURUMiCnRlYy5zdWIubWV0YSRDbHVzdGVyW3RlYy5zdWIubWV0YSRURklERi5DbHVzdGVyID09ICIzIl0gPC0gIk1hdHVyZSBjVEVDIgp0ZWMuc3ViLm1ldGEkQ2x1c3Rlclt0ZWMuc3ViLm1ldGEkVEZJREYuQ2x1c3RlciA9PSAiNyJdIDwtICJNYXR1cmUgbVRFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjEiXSA8LSAiUG9zdC1BaXJlIG1URUMiCnRlYy5zdWIubWV0YSRDbHVzdGVyW3RlYy5zdWIubWV0YSRURklERi5DbHVzdGVyID09ICI1Il0gPC0gIlR1ZnQtbGlrZSBtVEVDIgp0ZWMuc3ViLm1ldGEkQ2x1c3Rlclt0ZWMuc3ViLm1ldGEkVEZJREYuQ2x1c3RlciA9PSAiNiJdIDwtICJQcm9saWZlcmF0aW5nIFRFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjgiXSA8LSAiblRFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjEwIl0gPC0gInNURUMiCgppbnRlci5jb2xzIDwtIGMoIiM5OTcwYWIiLCAiIzM1OTc4ZiIsICIjQjBjZGMxIiwgIiM3NjJhODMiLCAiIzAxNjY1ZSIsICIjZTdkNGU4IiwgIiNkZmMyN2QiLCAiIzhjNTEwYSIgLCIjYmY4MTJkIikKbmFtZXMoaW50ZXIuY29scykgPC0gYygiUG9zdC1BaXJlIG1URUMiLCAnSW50ZXJ0eXBpY2FsIFRFQycsICdNYXR1cmUgY1RFQycsICdUdWZ0LWxpa2UgbVRFQycsIAogICAgICAgICAgICAgICAgICAgICAgICdQcm9saWZlcmF0aW5nIFRFQycsICdNYXR1cmUgbVRFQycsICduVEVDJywgJ1BlcmluYXRhbCBjVEVDJywgJ3NURUMnKQpgYGAKCmBgYHtyfQp0ZWMuZ2V4IDwtIHJlYWQudGFibGUoIn4vRHJvcGJveC9BZ2VpbmdFeHBlcmltZW50L0Zyb3plbi9UaHltdXNRQ19TRm5vcm0udHN2Lmd6IiwKICAgICAgICAgICAgICAgICAgICAgIHNlcD0iXHQiLCBoZWFkZXI9VFJVRSwgc3RyaW5nc0FzRmFjdG9ycz1GQUxTRSkKdGVjLnN1Yi5nZXggPC0gdGVjLmdleFssIGNvbG5hbWVzKHRlYy5nZXgpICVpbiUgdGVjLnN1Yi5tZXRhJFNhbXBsZV0KCnRlYy5odmdzIDwtIHJlYWQudGFibGUoIn4vRHJvcGJveC9BZ2VpbmdFeHBlcmltZW50L0Zyb3plbi9UaHltdXNfSFZHLnRzdiIsCiAgICAgICAgICAgICAgICAgICAgICAgc2VwPSJcdCIsIGhlYWRlcj1UUlVFLCBzdHJpbmdzQXNGYWN0b3JzPUZBTFNFKQpgYGAKCkknbGwgYnVpbGQgYSBrTk4tZ3JhcGggZnJvbSB0aGUgZmlyc3QgMzAgUENzIHByZXZpb3VzbHkgY29tcHV0ZWQgb24gYWxsIFRFQy4KCmBgYHtyfQpzZXQuc2VlZCg0MikKdGVjLmtubiA8LSBidWlsZEtOTkdyYXBoKHg9YXMubWF0cml4KHRlYy5zdWIubWV0YVssIHBhc3RlMCgiUEMiLCAxOjMwKV0pLCBrPTIxLCBkPU5BLCB0cmFuc3Bvc2VkPVRSVUUpCnRlYy5mci5sYXlvdXQgPC0gbGF5b3V0X3dpdGhfZnIodGVjLmtubikKcGxvdCh0ZWMua25uLCBsYXlvdXQ9dGVjLmZyLmxheW91dCwgdmVydGV4LmZyYW1lLmNvbG9yPSdza3libHVlJywgdmVydGV4LmNvbG9yPSdza3libHVlJywgdmVydGV4LmxhYmVsLmNvbG9yPSdibGFjaycsIAogICAgIHZlcnRleC5sYWJlbC5mYW1pbHk9J0hlbHZldGljYScsIGVkZ2UuY29sb3I9J2dyZXk2MCcsIHZlcnRleC5sYWJlbC5jZXg9MC45LAogICAgIHZlcnRleC5sYWJlbC5kaXN0PTEsIGVkZ2UuYXJyb3cuc2l6ZT0wLjIpCmBgYAoKVGhpcyBpcyBhIGZhaXJseSBkZW5zZWx5IGNvbm5lY3RlZCBuZXR3b3JrLCBob3cgZG9lcyB0aGUgVU1BUCBsb29rPwoKYGBge3J9CnNldC5zZWVkKDQyKQp0ZWMudW1hcCA8LSB1bWFwKGFzLm1hdHJpeCh0ZWMuc3ViLm1ldGFbLCBwYXN0ZTAoIlBDIiwgMTozMCldKSwKICAgICAgICAgICAgICAgICBuX2NvbXBvbmVudHM9MiwKICAgICAgICAgICAgICAgICBuX25laWdoYm9ycz0yMSwgbWV0cmljPSdldWNsaWRlYW4nLAogICAgICAgICAgICAgICAgIGluaXQ9J3JhbmRvbScsIG1pbl9kaXN0PTAuMikKdGVjLnVtYXAuZGYgPC0gYXMuZGF0YS5mcmFtZSh0ZWMudW1hcCRsYXlvdXQpCmNvbG5hbWVzKHRlYy51bWFwLmRmKSA8LSBjKCJVTUFQMSIsICJVTUFQMiIpCnRlYy51bWFwLmRmJFNhbXBsZSA8LSB0ZWMuc3ViLm1ldGEkU2FtcGxlCgp0ZWMudW1hcC5tZXJnZSA8LSBtZXJnZSh0ZWMudW1hcC5kZiwgdGVjLnN1Yi5tZXRhLCBieT0nU2FtcGxlJykKYGBgCgoKYGBge3IsIGZpZy53aWR0aD03Ljk1LCBmaWcuaGVpZ2h0PTQuMTV9CmdncGxvdCh0ZWMudW1hcC5tZXJnZSwgYWVzKHg9VU1BUDEsIHk9VU1BUDIpKSArCiAgZ2VvbV9wb2ludChhZXMoY29sb3VyPUNsdXN0ZXIpKSArCiAgdGhlbWVfY2xlYW4oKSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXM9aW50ZXIuY29scykgKwogIGZhY2V0X3dyYXAofkFnZSkgKwogIGd1aWRlcyhjb2xvdXI9Z3VpZGVfbGVnZW5kKG92ZXJyaWRlLmFlcz1saXN0KHNpemU9MykpLAogICAgICAgICBzaGFwZT1ndWlkZV9sZWdlbmQob3ZlcnJpZGUuYWVzPWxpc3Qoc2l6ZT0zKSkpCmBgYAoKCmBgYHtyfQojIHJhbmRvbWx5IHNlbGVjdCB2ZXJ0aWNlcyBpbiB0aGUgZ3JhcGgKbi5ob29kIDwtIDAuMDUKdGVjLnJhbmRvbS52ZXJ0aWNlcyA8LSBzYW1wbGUoVih0ZWMua25uKSwgc2l6ZT1mbG9vcihuLmhvb2QqbGVuZ3RoKFYodGVjLmtubikpKSkKIyBsb29wIG92ZXIgcmFuZG9tIHZlcnRpY2VzIGFuZCBjb3VudCB0aGUgbnVtYmVyIG9mIGNlbGxzIGluIGVhY2gKdGVjLnZlcnRleC5saXN0IDwtIHNhcHBseSgxOmxlbmd0aCh0ZWMucmFuZG9tLnZlcnRpY2VzKSwgRlVOPWZ1bmN0aW9uKFgpIG5laWdoYm9ycyh0ZWMua25uLCB2PXRlYy5yYW5kb20udmVydGljZXNbWF0pKQpoaXN0KHVubGlzdChsYXBwbHkodGVjLnZlcnRleC5saXN0LCBsZW5ndGgpKSwgMTAwLCBtYWluPSJIaXN0b2dyYW0gb2YgbmVpZ2hib3JzIiwgeGxhYj0iTmVpZ2hib3VyaG9vZCBzaXplIikKYGBgCgpUaGlzIGlzIHRoZSBoaXN0b2dyYW0gb2YgVEVDIG5laWdoYm91cmhvb2Qgc2l6ZXMuCgpgYGB7cn0KdGVjLnVtYXAubWVyZ2UkRXhwU2FtcCA8LSBwYXN0ZSh0ZWMudW1hcC5tZXJnZSRBZ2UsIHRlYy51bWFwLm1lcmdlJFNvcnREYXksIHNlcD0iXyIpCnRlYy51bWFwLm1lcmdlJFZlcnRleCA8LSBjKDE6bnJvdyh0ZWMudW1hcC5tZXJnZSkpCnRlYy5jb3VudHMgPC0gcXVhbnRfbmVpZ2hib3VyaG9vZChncmFwaD10ZWMua25uLCBtZXRhPXRlYy51bWFwLm1lcmdlLCBzYW1wbGUuY29sdW1uPSdFeHBTYW1wJywgc2FtcGxlLnZlcnRpY2VzPW4uaG9vZCkKYGBgCgoKYGBge3J9CnRlYy5yZXBzIDwtIHVubGlzdChsYXBwbHkoc3Ryc3BsaXQodW5pcXVlKHRlYy51bWFwLm1lcmdlJEV4cFNhbXApLCBzcGxpdD0iXyIpLCBGVU49ZnVuY3Rpb24oWCkgcGFzdGUwKFhbMl0pKSkKdGVjLmNvbmQgPC0gdW5saXN0KGxhcHBseShzdHJzcGxpdCh1bmlxdWUodGVjLnVtYXAubWVyZ2UkRXhwU2FtcCksIHNwbGl0PSJfIiksIEZVTj1mdW5jdGlvbihYKSBwYXN0ZTAoWFsxXSkpKQoKdGVjLnNhbXBsZS5tZXRhIDwtIGRhdGEuZnJhbWUoIkNvbmRpdGlvbiI9dGVjLmNvbmQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJSZXBsaWNhdGUiPXRlYy5yZXBzKQp0ZWMuc2FtcGxlLm1ldGEkU2FtcGxlIDwtIHBhc3RlKHRlYy5zYW1wbGUubWV0YSRDb25kaXRpb24sIHRlYy5zYW1wbGUubWV0YSRSZXBsaWNhdGUsIHNlcD0iXyIpCnJvd25hbWVzKHRlYy5zYW1wbGUubWV0YSkgPC0gdGVjLnNhbXBsZS5tZXRhJFNhbXBsZQoKdGVjLm1vZGVsIDwtIG1vZGVsLm1hdHJpeCh+IENvbmRpdGlvbiwgZGF0YT10ZWMuc2FtcGxlLm1ldGEpCmhlYWQodGVjLm1vZGVsKQpgYGAKCmBgYHtyfQp0ZWMuZGdlIDwtIERHRUxpc3QodGVjLmNvdW50c1ssIHJvd25hbWVzKHRlYy5tb2RlbCldLCBsaWIuc2l6ZT1sb2coY29sU3Vtcyh0ZWMuY291bnRzWywgcm93bmFtZXModGVjLm1vZGVsKV0pKSkKdGVjLmRnZSA8LSBlc3RpbWF0ZURpc3AodGVjLmRnZSwgdGVjLm1vZGVsKQp0ZWMuZml0IDwtIGdsbVFMRml0KHRlYy5kZ2UsIHRlYy5tb2RlbCwgcm9idXN0PVRSVUUpCiMgdGVjLmNvbnRyYXN0IDwtIG1ha2VDb250cmFzdHMoQ29uZGl0aW9uQSAtIENvbmRpdGlvbkIsIGxldmVscz10ZWMubW9kZWwpCiMgdGVjLnJlcyA8LSBnbG1RTEZUZXN0KHRlYy5maXQsIGNvbnRyYXN0PXRlYy5jb250cmFzdCkKCnRlYy5yZXMgPC0gYXMuZGF0YS5mcmFtZSh0b3BUYWdzKGdsbVFMRlRlc3QodGVjLmZpdCwgY29lZj0yKSwgc29ydC5ieT0nbm9uZScsIG49SW5mKSkKdGVjLnJlcyRTaWcgPC0gYXMuZmFjdG9yKGFzLm51bWVyaWModGVjLnJlcyRGRFIgPD0gMC4wMSkpCnRlYy5yZXMkTmVpZ2hib3VyaG9vZCA8LSBhcy5udW1lcmljKHJvd25hbWVzKHRlYy5yZXMpKQoKIyBjb250cm9sIHRoZSBzcGF0aWFsIEZEUgpxdmFscyA8LSB0ZWMucmVzJFBWYWx1ZQppcy5zaWcgPC0gcXZhbHMgPD0gMC4wMQpzdW1tYXJ5KGlzLnNpZykKYGBgCgpUaGVyZSBhcmUgMTggREEgbmVpZ2hib3VyaG9vZHMgLSBJIGV4cGVjdCB0aGVzZSBzaG91bGQgcmVmbGVjdCB0aGUgUGVyaW5hdGFsLCBJbnRlcnR5cGljYWwsIFByb2xpZmVyYXRpbmcgYW5kIHNURUMuIEknbGwgdXNlIHRoZSBkaXN0YW5jZS1iYXNlZCBhcHByb2FjaCB0byBjb3JyZWN0IGZvciAKdGhlIHNwYXRpYWwgRkRSLgoKYGBge3J9CnRlYy5zcGF0aWFsZmRyIDwtIGdyYXBoX3NwYXRpYWxGRFIobmVpZ2hib3Job29kcz10ZWMudmVydGV4Lmxpc3QsIGdyYXBoPXRlYy5rbm4sIGNvbm5lY3Rpdml0eT0iZGlzdGFuY2UiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBjYT1hcy5tYXRyaXgodGVjLnN1Yi5tZXRhWywgcGFzdGUwKCJQQyIsIDE6MzApXSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHZhbHVlcz10ZWMucmVzW29yZGVyKHRlYy5yZXMkTmVpZ2hib3VyaG9vZCksIF0kUFZhbHVlKQp0ZWMucmVzJFNwYXRpYWxGRFJbb3JkZXIodGVjLnJlcyROZWlnaGJvdXJob29kKV0gPC0gdGVjLnNwYXRpYWxmZHIKcXZhbHMgPC0gdGVjLnNwYXRpYWxmZHIKaXMuc2lnIDwtIHF2YWxzIDw9IDAuMDEKc3VtbWFyeShpcy5zaWcpCmBgYAoKSW50ZXJlc3RpbmcsIDMgbmVpZ2hib3VyaG9vZHMgYXJlIG5vIGxvbmdlciBzdGF0aXN0aWNhbGx5IHNpZ25pZmljYW50IGFmdGVyIHRoZSBzcGF0aWFsIEZEUiBjb3JyZWN0aW9uIC0gaG9wZWZ1bGx5IHRoZXNlIGFyZSBnZW51aW5lbHkgZmFsc2UtcG9zaXRpdmVzLgoKSW4gZWFjaCBuZWlnaGJvdXJob29kLCB3aGF0IGlzIHRoZSBtb3N0IGNvbW1vbiBjb25kaXRpb24gb3IgYmxvY2sgb2YgY2VsbHM/CgpgYGB7cn0KdGVjLm5laWdoYm91ci5leHBycyA8LSBuZWlnaGJvcmhvb2RfZXhwcmVzc2lvbih0ZWMudmVydGV4Lmxpc3QsIHRlYy5zdWIuZ2V4KQpgYGAKCkVtYmVkIHRoZXNlIGh5cGVyc3BoZXJlcyB3aXRoIGEgUENBIGFuZCBVTUFQLgoKYGBge3J9CnRlYy5uZWlnaGJvdXIucGNhIDwtIHByY29tcCgodCh0ZWMubmVpZ2hib3VyLmV4cHJzW3RlYy5odmdzJEhWRywgXSkpKQpwYWlycyh0ZWMubmVpZ2hib3VyLnBjYSR4WywgYygxOjUpXSkKYGBgCgpgYGB7cn0Kc2V0LnNlZWQoNDIpCm5laWdoYm91cmhvb2QudW1hcCA8LSB1bWFwKHRlYy5uZWlnaGJvdXIucGNhJHhbLCBjKDE6MzApXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgbl9jb21wb25lbnRzPTIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG5fbmVpZ2hib3JzPTIxLCBtZXRyaWM9J2V1Y2xpZGVhbicsCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGluaXQ9J3JhbmRvbScsIG1pbl9kaXN0PTAuMSkKYGBgCgpXZSBjYW4gb3ZlcmxheSB0aGUgREEgdGVzdGluZyBvbiB0aGVzZSBuZWlnaGJvdXJob29kcy4KCmBgYHtyfQp0ZWMubmVpZ2hib3IuZGYgPC0gdGVjLnJlc1ssIGMoImxvZ0ZDIiwgIk5laWdoYm91cmhvb2QiLCAiU3BhdGlhbEZEUiIpXQp0ZWMubmVpZ2hib3IuZGYgPC0gZG8uY2FsbChjYmluZC5kYXRhLmZyYW1lLCBsaXN0KHRlYy5uZWlnaGJvci5kZiwgYXMuZGF0YS5mcmFtZShuZWlnaGJvdXJob29kLnVtYXAkbGF5b3V0KSkpCmNvbG5hbWVzKHRlYy5uZWlnaGJvci5kZikgPC0gYygibG9nRkMiLCAiTmVpZ2hib3VyaG9vZCIsICJTcGF0aWFsRkRSIiwgIlVNQVAxIiwgIlVNQVAyIikKdGVjLm5laWdoYm9yLmRmJFNpZyA8LSBhcy5udW1lcmljKHRlYy5uZWlnaGJvci5kZiRTcGF0aWFsRkRSIDw9IDAuMDUpCgpnZ3Bsb3QodGVjLm5laWdoYm9yLmRmLCBhZXMoeD1VTUFQMSwgeT1VTUFQMikpICsKICBnZW9tX3BvaW50KGRhdGE9dGVjLm5laWdoYm9yLmRmW3RlYy5uZWlnaGJvci5kZiRTaWcgPT0gMCwgXSwKICAgICAgICAgICAgIGNvbG91cj0nZ3JleTgwJywgc2l6ZT0yKSArCiAgZ2VvbV9wb2ludChkYXRhPXRlYy5uZWlnaGJvci5kZlt0ZWMubmVpZ2hib3IuZGYkU2lnID09IDEsIF0sCiAgICAgICAgICAgICBhZXMoY29sb3VyPWxvZ0ZDKSwgc2l6ZT00KSArCiAgdGhlbWVfY2xlYW4oKSArCiAgc2NhbGVfY29sb3VyX2dyYWRpZW50Mihsb3c9ImJsdWUiLCBtaWQ9ImdyZXk4MCIsIGhpZ2g9InJlZCIpCmBgYAoKU29tZSBhcmUgdXAgYW5kIHNvbWUgYXJlIGRvd24uIFdoaWNoIFRFQyBzdWJ0eXBlcyBkbyB0aGV5IGxhcmdlbHkgY29ycmVzcG9uZCB3aXRoPwoKYGBge3J9CnRlYy5uZWlnaGJvdXIubGlzdCA8LSBsaXN0KCkKZm9yKHggaW4gc2VxX2Fsb25nKDE6bGVuZ3RoKHRlYy52ZXJ0ZXgubGlzdCkpKXsKICB4LmRmIDwtIHRlYy51bWFwLm1lcmdlW3RlYy51bWFwLm1lcmdlJFZlcnRleCAlaW4lIHRlYy52ZXJ0ZXgubGlzdFtbeF1dLCBdCiAgeC5yZXAgPC0gbmFtZXModGFibGUoeC5kZiRTb3J0RGF5KSlbd2hpY2godGFibGUoeC5kZiRTb3J0RGF5KSA9PSBtYXgodGFibGUoeC5kZiRTb3J0RGF5KSkpXQogIGlmKGxlbmd0aCh4LnJlcCkgPiAxKXsKICAgIHgucmVwIDwtIHNhbXBsZShzaXplPTEsIHgucmVwKQogIH0KICB4LmJsb2NrIDwtIG5hbWVzKHRhYmxlKHguZGYkQ2x1c3RlcikpW3doaWNoKHRhYmxlKHguZGYkQ2x1c3RlcikgPT0gbWF4KHRhYmxlKHguZGYkQ2x1c3RlcikpKV0KICAgIGlmKGxlbmd0aCh4LmJsb2NrKSA+IDEpewogICAgeC5ibG9jayA8LSBzYW1wbGUoc2l6ZT0xLCB4LmJsb2NrKQogIH0KICB4LmNvbmRpdGlvbiA8LSBuYW1lcyh0YWJsZSh4LmRmJEFnZSkpW3doaWNoKHRhYmxlKHguZGYkQWdlKSA9PSBtYXgodGFibGUoeC5kZiRBZ2UpKSldCiAgICBpZihsZW5ndGgoeC5jb25kaXRpb24pID4gMSl7CiAgICB4LmNvbmRpdGlvbiA8LSBzYW1wbGUoc2l6ZT0xLCB4LmNvbmRpdGlvbikKICB9CiAgCiAgdGVjLm5laWdoYm91ci5saXN0W1t4XV0gPC0gZGF0YS5mcmFtZSgiUmVwbGljYXRlIj14LnJlcCwgIkNsdXN0ZXIiPXguYmxvY2ssICJDb25kaXRpb24iPXguY29uZGl0aW9uLCAiTmVpZ2hib3VyaG9vZCI9eCkKfQoKdGVjLm5laWdoYm91ci5tZXRhIDwtIGRvLmNhbGwocmJpbmQuZGF0YS5mcmFtZSwgdGVjLm5laWdoYm91ci5saXN0KQp0ZWMubmVpZ2hib3VyLm1lcmdlIDwtIG1lcmdlKHRlYy5uZWlnaGJvci5kZiwgdGVjLm5laWdoYm91ci5tZXRhLCBieT0nTmVpZ2hib3VyaG9vZCcpCgp0ZWMubmVpZ2hib3VyLm1lcmdlJERpZmYgPC0gc2lnbih0ZWMubmVpZ2hib3VyLm1lcmdlJGxvZ0ZDKQp0ZWMubmVpZ2hib3VyLm1lcmdlJERpZmZbdGVjLm5laWdoYm91ci5tZXJnZSRTaWcgPT0gMF0gPC0gMApgYGAKCgpgYGB7ciwgZmlnLndpZHRoPTkuNzUsIGZpZy5oZWlnaHQ9NC41NX0KZ2dwbG90KHRlYy5uZWlnaGJvdXIubWVyZ2UsIGFlcyh4PVVNQVAxLCB5PVVNQVAyKSkgKwogIGdlb21fcG9pbnQoZGF0YT10ZWMubmVpZ2hib3VyLm1lcmdlWywgYygiVU1BUDEiLCAiVU1BUDIiKV0sCiAgICAgICAgICAgICBjb2xvdXI9J2dyZXk4MCcsIHNpemU9MSkgKwogIGdlb21fcG9pbnQoZGF0YT10ZWMubmVpZ2hib3VyLm1lcmdlW3RlYy5uZWlnaGJvdXIubWVyZ2UkU2lnID09IDEsIF0sCiAgICAgICAgICAgICBhZXMoY29sb3VyPWxvZ0ZDKSwgc2l6ZT00KSArCiAgdGhlbWVfY2xlYW4oKSArCiAgc2NhbGVfY29sb3VyX2dyYWRpZW50Mihsb3c9ImJsdWUiLCBtaWQ9ImdyZXk4MCIsIGhpZ2g9InJlZCIpICsKICBmYWNldF93cmFwKH5DbHVzdGVyKQpgYGAKClRoZXJlIGlzIGEgc3Vic2V0IG9mIHRoZSBQcm9saWZlcmF0aW5nIFRFQyB0aGF0IGFyZSBkb3duLCBnb29kLCBhcyBhcmUgdGhlIFBlcmluYXRhbCBjVEVDLiBMaWtld2lzZSwgbW9zdCBvZiB0aGUgSW50ZXJ0eXBpY2FsIFRFQyBhcmUgdXAgYXMgd2VsbCwgYnV0IHNvbWUgYXJlIGFsc28gZG93bi4gCkkgZG9uJ3Qga25vdyBpZiB0aGlzIGlzIGJlY2F1c2Ugb2YgYSBjb21wb3NpdGlvbmFsIGVmZmVjdCBvciBiZWNhdXNlIHRoZXNlIGFyZSB0aGUgb25lcyB0aGF0IHdvdWxkIGJlIGRpZmZlcmVudGlhdGluZyBpbnRvIG1URUMgdmlhIHRoZSBQcm9saWZlcmF0aW5nIFRFQyBjb21wYXJ0bWVudC4KCmBgYHtyfQp0YWJsZSh0ZWMubmVpZ2hib3VyLm1lcmdlJENsdXN0ZXIsIHRlYy5uZWlnaGJvdXIubWVyZ2UkRGlmZikKYGBgCgpJIHdvdWxkIHNheSB0aGF0IHRoZXNlIHJlc3VsdHMgbWFrZSBhIGxvdCBvZiBzZW5zZS4gQ2FuIEkgbm93IGV4dGVuZCB0aGlzIHRvIGluY2x1ZGUgYWxsIHRpbWUgcG9pbnRzIGFuZCBmaXQgYWdlIGFzIGEgbGluZWFyIG9yZGluYWwgdmFyaWFibGU/CgojIyBFeHRlbmRlZCBURUMgREEgdGVzdGluZy4KCmBgYHtyfQojIGV4Y2x1ZGUgdGVjaG5pY2FsIGFydGlmYWN0IGNsdXN0ZXIKdGVjLm1ldGEgPC0gdGVjLm1ldGFbIXRlYy5tZXRhJFRGSURGLkNsdXN0ZXIgJWluJSBjKDQpLCBdCnRlYy5zdWIubWV0YSA8LSB0ZWMubWV0YQp0ZWMuc3ViLm1ldGEkQWdlRmFjdG9yIDwtIG9yZGVyZWQodGVjLnN1Yi5tZXRhJEFnZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxldmVscz1jKCIxd2siLCAiNHdrIiwgIjE2d2siLCAiMzJ3ayIsICI1MndrIikpCiMgYWRkIHRoZSBsYWJlbCBhbm5vdGF0aW9uCnRlYy5zdWIubWV0YSRDbHVzdGVyIDwtICJVbmtub3duIgp0ZWMuc3ViLm1ldGEkQ2x1c3Rlclt0ZWMuc3ViLm1ldGEkVEZJREYuQ2x1c3RlciA9PSAiMiJdIDwtICJJbnRlcnR5cGljYWwgVEVDIgp0ZWMuc3ViLm1ldGEkQ2x1c3Rlclt0ZWMuc3ViLm1ldGEkVEZJREYuQ2x1c3RlciA9PSAiOSJdIDwtICJQZXJpbmF0YWwgY1RFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjMiXSA8LSAiTWF0dXJlIGNURUMiCnRlYy5zdWIubWV0YSRDbHVzdGVyW3RlYy5zdWIubWV0YSRURklERi5DbHVzdGVyID09ICI3Il0gPC0gIk1hdHVyZSBtVEVDIgp0ZWMuc3ViLm1ldGEkQ2x1c3Rlclt0ZWMuc3ViLm1ldGEkVEZJREYuQ2x1c3RlciA9PSAiMSJdIDwtICJQb3N0LUFpcmUgbVRFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjUiXSA8LSAiVHVmdC1saWtlIG1URUMiCnRlYy5zdWIubWV0YSRDbHVzdGVyW3RlYy5zdWIubWV0YSRURklERi5DbHVzdGVyID09ICI2Il0gPC0gIlByb2xpZmVyYXRpbmcgVEVDIgp0ZWMuc3ViLm1ldGEkQ2x1c3Rlclt0ZWMuc3ViLm1ldGEkVEZJREYuQ2x1c3RlciA9PSAiOCJdIDwtICJuVEVDIgp0ZWMuc3ViLm1ldGEkQ2x1c3Rlclt0ZWMuc3ViLm1ldGEkVEZJREYuQ2x1c3RlciA9PSAiMTAiXSA8LSAic1RFQyIKCmludGVyLmNvbHMgPC0gYygiIzk5NzBhYiIsICIjMzU5NzhmIiwgIiNCMGNkYzEiLCAiIzc2MmE4MyIsICIjMDE2NjVlIiwgIiNlN2Q0ZTgiLCAiI2RmYzI3ZCIsICIjOGM1MTBhIiAsIiNiZjgxMmQiKQpuYW1lcyhpbnRlci5jb2xzKSA8LSBjKCJQb3N0LUFpcmUgbVRFQyIsICdJbnRlcnR5cGljYWwgVEVDJywgJ01hdHVyZSBjVEVDJywgJ1R1ZnQtbGlrZSBtVEVDJywgCiAgICAgICAgICAgICAgICAgICAgICAgJ1Byb2xpZmVyYXRpbmcgVEVDJywgJ01hdHVyZSBtVEVDJywgJ25URUMnLCAnUGVyaW5hdGFsIGNURUMnLCAnc1RFQycpCmBgYAoKYGBge3J9CnRlYy5zdWIuZ2V4IDwtIHRlYy5nZXhbLCBjb2xuYW1lcyh0ZWMuZ2V4KSAlaW4lIHRlYy5zdWIubWV0YSRTYW1wbGVdCmBgYAoKSSdsbCBidWlsZCBhIGtOTi1ncmFwaCBmcm9tIHRoZSBmaXJzdCAzMCBQQ3MgcHJldmlvdXNseSBjb21wdXRlZCBvbiBhbGwgVEVDLgoKYGBge3J9CnNldC5zZWVkKDQyKQp0ZWMua25uIDwtIGJ1aWxkS05OR3JhcGgoeD1hcy5tYXRyaXgodGVjLnN1Yi5tZXRhWywgcGFzdGUwKCJQQyIsIDE6MzApXSksIGs9MjEsIGQ9TkEsIHRyYW5zcG9zZWQ9VFJVRSkKdGVjLmZyLmxheW91dCA8LSBsYXlvdXRfd2l0aF9mcih0ZWMua25uKQpwbG90KHRlYy5rbm4sIGxheW91dD10ZWMuZnIubGF5b3V0LCB2ZXJ0ZXguZnJhbWUuY29sb3I9J3NreWJsdWUnLCB2ZXJ0ZXguY29sb3I9J3NreWJsdWUnLCB2ZXJ0ZXgubGFiZWwuY29sb3I9J2JsYWNrJywgCiAgICAgdmVydGV4LmxhYmVsLmZhbWlseT0nSGVsdmV0aWNhJywgZWRnZS5jb2xvcj0nZ3JleTYwJywgdmVydGV4LmxhYmVsLmNleD0wLjksCiAgICAgdmVydGV4LmxhYmVsLmRpc3Q9MSwgZWRnZS5hcnJvdy5zaXplPTAuMikKYGBgCgpUaGlzIGlzIGEgZmFpcmx5IGRlbnNlbHkgY29ubmVjdGVkIG5ldHdvcmssIGhvdyBkb2VzIHRoZSBVTUFQIGxvb2s/CgpgYGB7cn0Kc2V0LnNlZWQoNDIpCnRlYy51bWFwIDwtIHVtYXAoYXMubWF0cml4KHRlYy5zdWIubWV0YVssIHBhc3RlMCgiUEMiLCAxOjMwKV0pLAogICAgICAgICAgICAgICAgIG5fY29tcG9uZW50cz0yLAogICAgICAgICAgICAgICAgIG5fbmVpZ2hib3JzPTIxLCBtZXRyaWM9J2V1Y2xpZGVhbicsCiAgICAgICAgICAgICAgICAgaW5pdD0ncmFuZG9tJywgbWluX2Rpc3Q9MC4yKQp0ZWMudW1hcC5kZiA8LSBhcy5kYXRhLmZyYW1lKHRlYy51bWFwJGxheW91dCkKY29sbmFtZXModGVjLnVtYXAuZGYpIDwtIGMoIlVNQVAxIiwgIlVNQVAyIikKdGVjLnVtYXAuZGYkU2FtcGxlIDwtIHRlYy5zdWIubWV0YSRTYW1wbGUKCnRlYy51bWFwLm1lcmdlIDwtIG1lcmdlKHRlYy51bWFwLmRmLCB0ZWMuc3ViLm1ldGEsIGJ5PSdTYW1wbGUnKQpgYGAKCgpgYGB7ciwgZmlnLndpZHRoPTkuOTUsIGZpZy5oZWlnaHQ9Ny4xNX0KZ2dwbG90KHRlYy51bWFwLm1lcmdlLCBhZXMoeD1VTUFQMSwgeT1VTUFQMikpICsKICBnZW9tX3BvaW50KGFlcyhjb2xvdXI9Q2x1c3RlcikpICsKICB0aGVtZV9jbGVhbigpICsKICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcz1pbnRlci5jb2xzKSArCiAgZmFjZXRfd3JhcCh+QWdlRmFjdG9yKSArCiAgZ3VpZGVzKGNvbG91cj1ndWlkZV9sZWdlbmQob3ZlcnJpZGUuYWVzPWxpc3Qoc2l6ZT0zKSksCiAgICAgICAgIHNoYXBlPWd1aWRlX2xlZ2VuZChvdmVycmlkZS5hZXM9bGlzdChzaXplPTMpKSkKYGBgCgoKYGBge3J9CiMgcmFuZG9tbHkgc2VsZWN0IHZlcnRpY2VzIGluIHRoZSBncmFwaApuLmhvb2QgPC0gMC4wNQp0ZWMucmFuZG9tLnZlcnRpY2VzIDwtIHNhbXBsZShWKHRlYy5rbm4pLCBzaXplPWZsb29yKG4uaG9vZCpsZW5ndGgoVih0ZWMua25uKSkpKQojIGxvb3Agb3ZlciByYW5kb20gdmVydGljZXMgYW5kIGNvdW50IHRoZSBudW1iZXIgb2YgY2VsbHMgaW4gZWFjaAp0ZWMudmVydGV4Lmxpc3QgPC0gc2FwcGx5KDE6bGVuZ3RoKHRlYy5yYW5kb20udmVydGljZXMpLCBGVU49ZnVuY3Rpb24oWCkgbmVpZ2hib3JzKHRlYy5rbm4sIHY9dGVjLnJhbmRvbS52ZXJ0aWNlc1tYXSkpCmhpc3QodW5saXN0KGxhcHBseSh0ZWMudmVydGV4Lmxpc3QsIGxlbmd0aCkpLCAxMDAsIG1haW49Ikhpc3RvZ3JhbSBvZiBuZWlnaGJvcnMiLCB4bGFiPSJOZWlnaGJvdXJob29kIHNpemUiKQpgYGAKClRoaXMgaXMgdGhlIGhpc3RvZ3JhbSBvZiBURUMgbmVpZ2hib3VyaG9vZCBzaXplcy4KCmBgYHtyfQp0ZWMudW1hcC5tZXJnZSRFeHBTYW1wIDwtIHBhc3RlKHRlYy51bWFwLm1lcmdlJEFnZSwgdGVjLnVtYXAubWVyZ2UkU29ydERheSwgc2VwPSJfIikKdGVjLnVtYXAubWVyZ2UkVmVydGV4IDwtIGMoMTpucm93KHRlYy51bWFwLm1lcmdlKSkKdGVjLmNvdW50cyA8LSBxdWFudF9uZWlnaGJvdXJob29kKGdyYXBoPXRlYy5rbm4sIG1ldGE9dGVjLnVtYXAubWVyZ2UsIHNhbXBsZS5jb2x1bW49J0V4cFNhbXAnLCBzYW1wbGUudmVydGljZXM9bi5ob29kKQpgYGAKCgpgYGB7cn0KdGVjLnJlcHMgPC0gdW5saXN0KGxhcHBseShzdHJzcGxpdCh1bmlxdWUodGVjLnVtYXAubWVyZ2UkRXhwU2FtcCksIHNwbGl0PSJfIiksIEZVTj1mdW5jdGlvbihYKSBwYXN0ZTAoWFsyXSkpKQp0ZWMuY29uZCA8LSB1bmxpc3QobGFwcGx5KHN0cnNwbGl0KHVuaXF1ZSh0ZWMudW1hcC5tZXJnZSRFeHBTYW1wKSwgc3BsaXQ9Il8iKSwgRlVOPWZ1bmN0aW9uKFgpIHBhc3RlMChYWzFdKSkpCgp0ZWMuc2FtcGxlLm1ldGEgPC0gZGF0YS5mcmFtZSgiQ29uZGl0aW9uIj10ZWMuY29uZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlJlcGxpY2F0ZSI9dGVjLnJlcHMpCnRlYy5zYW1wbGUubWV0YSRTYW1wbGUgPC0gcGFzdGUodGVjLnNhbXBsZS5tZXRhJENvbmRpdGlvbiwgdGVjLnNhbXBsZS5tZXRhJFJlcGxpY2F0ZSwgc2VwPSJfIikKcm93bmFtZXModGVjLnNhbXBsZS5tZXRhKSA8LSB0ZWMuc2FtcGxlLm1ldGEkU2FtcGxlCnRlYy5zYW1wbGUubWV0YSRDb25kaXRpb24gPC0gb3JkZXJlZCh0ZWMuc2FtcGxlLm1ldGEkQ29uZGl0aW9uLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGV2ZWxzPWMoIjF3ayIsICI0d2siLCAiMTZ3ayIsICIzMndrIiwgIjUyd2siKSkKdGVjLm1vZGVsIDwtIG1vZGVsLm1hdHJpeCh+IENvbmRpdGlvbiwgZGF0YT10ZWMuc2FtcGxlLm1ldGEpCmhlYWQodGVjLm1vZGVsKQpgYGAKCmBgYHtyfQp0ZWMuZGdlIDwtIERHRUxpc3QodGVjLmNvdW50c1ssIHJvd25hbWVzKHRlYy5tb2RlbCldLCBsaWIuc2l6ZT1sb2coY29sU3Vtcyh0ZWMuY291bnRzWywgcm93bmFtZXModGVjLm1vZGVsKV0pKSkKdGVjLmRnZSA8LSBlc3RpbWF0ZURpc3AodGVjLmRnZSwgdGVjLm1vZGVsKQp0ZWMuZml0IDwtIGdsbVFMRml0KHRlYy5kZ2UsIHRlYy5tb2RlbCwgcm9idXN0PVRSVUUpCiMgdGVjLmNvbnRyYXN0IDwtIG1ha2VDb250cmFzdHMoQ29uZGl0aW9uQSAtIENvbmRpdGlvbkIsIGxldmVscz10ZWMubW9kZWwpCiMgdGVjLnJlcyA8LSBnbG1RTEZUZXN0KHRlYy5maXQsIGNvbnRyYXN0PXRlYy5jb250cmFzdCkKCnRlYy5yZXMgPC0gYXMuZGF0YS5mcmFtZSh0b3BUYWdzKGdsbVFMRlRlc3QodGVjLmZpdCwgY29lZj0yKSwgc29ydC5ieT0nbm9uZScsIG49SW5mKSkKdGVjLnJlcyRTaWcgPC0gYXMuZmFjdG9yKGFzLm51bWVyaWModGVjLnJlcyRGRFIgPD0gMC4wMSkpCnRlYy5yZXMkTmVpZ2hib3VyaG9vZCA8LSBhcy5udW1lcmljKHJvd25hbWVzKHRlYy5yZXMpKQoKIyBjb250cm9sIHRoZSBzcGF0aWFsIEZEUgpxdmFscyA8LSB0ZWMucmVzJFBWYWx1ZQppcy5zaWcgPC0gcXZhbHMgPD0gMC4wMQpzdW1tYXJ5KGlzLnNpZykKYGBgCgpUaGVyZSBhcmUgNDYgREEgbmVpZ2hib3VyaG9vZHMgLSBJIGV4cGVjdCB0aGVzZSBzaG91bGQgcmVmbGVjdCB0aGUgUGVyaW5hdGFsLCBJbnRlcnR5cGljYWwsIFByb2xpZmVyYXRpbmcgYW5kIHNURUMuIEknbGwgdXNlIHRoZSBkaXN0YW5jZS1iYXNlZCBhcHByb2FjaCB0byAKY29ycmVjdCBmb3IgdGhlIHNwYXRpYWwgRkRSLgoKYGBge3J9CnRlYy5zcGF0aWFsZmRyIDwtIGdyYXBoX3NwYXRpYWxGRFIobmVpZ2hib3Job29kcz10ZWMudmVydGV4Lmxpc3QsIGdyYXBoPXRlYy5rbm4sIGNvbm5lY3Rpdml0eT0iZGlzdGFuY2UiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBjYT1hcy5tYXRyaXgodGVjLnN1Yi5tZXRhWywgcGFzdGUwKCJQQyIsIDE6MzApXSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHZhbHVlcz10ZWMucmVzW29yZGVyKHRlYy5yZXMkTmVpZ2hib3VyaG9vZCksIF0kUFZhbHVlKQp0ZWMucmVzJFNwYXRpYWxGRFJbb3JkZXIodGVjLnJlcyROZWlnaGJvdXJob29kKV0gPC0gdGVjLnNwYXRpYWxmZHIKcXZhbHMgPC0gdGVjLnNwYXRpYWxmZHIKaXMuc2lnIDwtIHF2YWxzIDw9IDAuMDEKc3VtbWFyeShpcy5zaWcpCmBgYAoKSW50ZXJlc3RpbmcsIDEyIG5laWdoYm91cmhvb2RzIGFyZSBubyBsb25nZXIgc3RhdGlzdGljYWxseSBzaWduaWZpY2FudCBhZnRlciB0aGUgc3BhdGlhbCBGRFIgY29ycmVjdGlvbiAtIGhvcGVmdWxseSB0aGVzZSBhcmUgZ2VudWluZWx5IGZhbHNlLXBvc2l0aXZlcy4KCkluIGVhY2ggbmVpZ2hib3VyaG9vZCwgd2hhdCBpcyB0aGUgbW9zdCBjb21tb24gY29uZGl0aW9uIG9yIGJsb2NrIG9mIGNlbGxzPwoKYGBge3J9CnRlYy5uZWlnaGJvdXIuZXhwcnMgPC0gbmVpZ2hib3Job29kX2V4cHJlc3Npb24odGVjLnZlcnRleC5saXN0LCB0ZWMuc3ViLmdleCkKYGBgCgpFbWJlZCB0aGVzZSBoeXBlcnNwaGVyZXMgd2l0aCBhIFBDQSBhbmQgVU1BUC4KCmBgYHtyfQp0ZWMubmVpZ2hib3VyLnBjYSA8LSBwcmNvbXAoKHQodGVjLm5laWdoYm91ci5leHByc1t0ZWMuaHZncyRIVkcsIF0pKSkKcGFpcnModGVjLm5laWdoYm91ci5wY2EkeFssIGMoMTo1KV0pCmBgYAoKYGBge3J9CnNldC5zZWVkKDQyKQpuZWlnaGJvdXJob29kLnVtYXAgPC0gdW1hcCh0ZWMubmVpZ2hib3VyLnBjYSR4WywgYygxOjMwKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG5fY29tcG9uZW50cz0yLAogICAgICAgICAgICAgICAgICAgICAgICAgICBuX25laWdoYm9ycz0yMSwgbWV0cmljPSdldWNsaWRlYW4nLAogICAgICAgICAgICAgICAgICAgICAgICAgICBpbml0PSdyYW5kb20nLCBtaW5fZGlzdD0wLjEpCmBgYAoKV2UgY2FuIG92ZXJsYXkgdGhlIERBIHRlc3Rpbmcgb24gdGhlc2UgbmVpZ2hib3VyaG9vZHMuCgpgYGB7cn0KdGVjLm5laWdoYm9yLmRmIDwtIHRlYy5yZXNbLCBjKCJsb2dGQyIsICJOZWlnaGJvdXJob29kIiwgIlNwYXRpYWxGRFIiKV0KdGVjLm5laWdoYm9yLmRmIDwtIGRvLmNhbGwoY2JpbmQuZGF0YS5mcmFtZSwgbGlzdCh0ZWMubmVpZ2hib3IuZGYsIGFzLmRhdGEuZnJhbWUobmVpZ2hib3VyaG9vZC51bWFwJGxheW91dCkpKQpjb2xuYW1lcyh0ZWMubmVpZ2hib3IuZGYpIDwtIGMoImxvZ0ZDIiwgIk5laWdoYm91cmhvb2QiLCAiU3BhdGlhbEZEUiIsICJVTUFQMSIsICJVTUFQMiIpCnRlYy5uZWlnaGJvci5kZiRTaWcgPC0gYXMubnVtZXJpYyh0ZWMubmVpZ2hib3IuZGYkU3BhdGlhbEZEUiA8PSAwLjA1KQoKZ2dwbG90KHRlYy5uZWlnaGJvci5kZiwgYWVzKHg9VU1BUDEsIHk9VU1BUDIpKSArCiAgZ2VvbV9wb2ludChkYXRhPXRlYy5uZWlnaGJvci5kZlt0ZWMubmVpZ2hib3IuZGYkU2lnID09IDAsIF0sCiAgICAgICAgICAgICBjb2xvdXI9J2dyZXk4MCcsIHNpemU9MikgKwogIGdlb21fcG9pbnQoZGF0YT10ZWMubmVpZ2hib3IuZGZbdGVjLm5laWdoYm9yLmRmJFNpZyA9PSAxLCBdLAogICAgICAgICAgICAgYWVzKGNvbG91cj1sb2dGQyksIHNpemU9NCkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9ncmFkaWVudDIobG93PSJibHVlIiwgbWlkPSJncmV5ODAiLCBoaWdoPSJyZWQiKQpgYGAKClNvbWUgYXJlIHVwIGFuZCBzb21lIGFyZSBkb3duLiBXaGljaCBURUMgc3VidHlwZXMgZG8gdGhleSBsYXJnZWx5IGNvcnJlc3BvbmQgd2l0aD8gVGhhdCBiaWcgc3RyZWFrIG9mIGxvd2VyIGFidW5kYW5jZSBuZWlnaGJvdXJob29kcyBzaG91bGQgYmUgdGhlIGRpZmZlcmVudGlhdGlvbiAKdHJhamVjdG9yeSBmcm9tIEludGVydHlwaWNhbCB0byBNYXR1cmUgbVRFQy4KCmBgYHtyfQp0ZWMubmVpZ2hib3VyLmxpc3QgPC0gbGlzdCgpCmZvcih4IGluIHNlcV9hbG9uZygxOmxlbmd0aCh0ZWMudmVydGV4Lmxpc3QpKSl7CiAgeC5kZiA8LSB0ZWMudW1hcC5tZXJnZVt0ZWMudW1hcC5tZXJnZSRWZXJ0ZXggJWluJSB0ZWMudmVydGV4Lmxpc3RbW3hdXSwgXQogIHgucmVwIDwtIG5hbWVzKHRhYmxlKHguZGYkU29ydERheSkpW3doaWNoKHRhYmxlKHguZGYkU29ydERheSkgPT0gbWF4KHRhYmxlKHguZGYkU29ydERheSkpKV0KICBpZihsZW5ndGgoeC5yZXApID4gMSl7CiAgICB4LnJlcCA8LSBzYW1wbGUoc2l6ZT0xLCB4LnJlcCkKICB9CiAgeC5ibG9jayA8LSBuYW1lcyh0YWJsZSh4LmRmJENsdXN0ZXIpKVt3aGljaCh0YWJsZSh4LmRmJENsdXN0ZXIpID09IG1heCh0YWJsZSh4LmRmJENsdXN0ZXIpKSldCiAgICBpZihsZW5ndGgoeC5ibG9jaykgPiAxKXsKICAgIHguYmxvY2sgPC0gc2FtcGxlKHNpemU9MSwgeC5ibG9jaykKICB9CiAgeC5jb25kaXRpb24gPC0gbmFtZXModGFibGUoeC5kZiRBZ2UpKVt3aGljaCh0YWJsZSh4LmRmJEFnZSkgPT0gbWF4KHRhYmxlKHguZGYkQWdlKSkpXQogICAgaWYobGVuZ3RoKHguY29uZGl0aW9uKSA+IDEpewogICAgeC5jb25kaXRpb24gPC0gc2FtcGxlKHNpemU9MSwgeC5jb25kaXRpb24pCiAgfQogIAogIHRlYy5uZWlnaGJvdXIubGlzdFtbeF1dIDwtIGRhdGEuZnJhbWUoIlJlcGxpY2F0ZSI9eC5yZXAsICJDbHVzdGVyIj14LmJsb2NrLCAiQ29uZGl0aW9uIj14LmNvbmRpdGlvbiwgIk5laWdoYm91cmhvb2QiPXgpCn0KCnRlYy5uZWlnaGJvdXIubWV0YSA8LSBkby5jYWxsKHJiaW5kLmRhdGEuZnJhbWUsIHRlYy5uZWlnaGJvdXIubGlzdCkKdGVjLm5laWdoYm91ci5tZXJnZSA8LSBtZXJnZSh0ZWMubmVpZ2hib3IuZGYsIHRlYy5uZWlnaGJvdXIubWV0YSwgYnk9J05laWdoYm91cmhvb2QnKQoKdGVjLm5laWdoYm91ci5tZXJnZSREaWZmIDwtIHNpZ24odGVjLm5laWdoYm91ci5tZXJnZSRsb2dGQykKdGVjLm5laWdoYm91ci5tZXJnZSREaWZmW3RlYy5uZWlnaGJvdXIubWVyZ2UkU2lnID09IDBdIDwtIDAKYGBgCgoKYGBge3IsIGZpZy53aWR0aD05Ljc1LCBmaWcuaGVpZ2h0PTUuMTV9CmdncGxvdCh0ZWMubmVpZ2hib3VyLm1lcmdlLCBhZXMoeD1VTUFQMSwgeT1VTUFQMikpICsKICBnZW9tX3BvaW50KGRhdGE9dGVjLm5laWdoYm91ci5tZXJnZVssIGMoIlVNQVAxIiwgIlVNQVAyIildLAogICAgICAgICAgICAgY29sb3VyPSdncmV5ODAnLCBzaXplPTEpICsKICBnZW9tX3BvaW50KGRhdGE9dGVjLm5laWdoYm91ci5tZXJnZVt0ZWMubmVpZ2hib3VyLm1lcmdlJFNpZyA9PSAxLCBdLAogICAgICAgICAgICAgYWVzKGNvbG91cj1sb2dGQyksIHNpemU9NCkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9ncmFkaWVudDIobG93PSJibHVlIiwgbWlkPSJncmV5ODAiLCBoaWdoPSJyZWQiKSArCiAgZmFjZXRfd3JhcCh+Q2x1c3RlcikKYGBgCgpUaGlzIHZlcnkgbmljZWx5IHJlY2FwaXR1bGF0ZXMgdGhlIERBIHRlc3RpbmcgdXNpbmcgY2x1c3RlcnMsIGFuZCBpdCBwaW5wb2ludHMgdGhlIGxvc3Mgb2YgbWVkdWxsYS1iaWFzZWQgSW50ZXJ0eXBpY2FsIFRFQyB3aGljaCB3ZSBvbmx5IHJlYWxseSBvYnNlcnZlZCBpbiBvdXIgbGFyZ2VyIApleHBlcmltZW50cy4gVGhpcyBpcyB3b3JraW5nIGJleW9uZCBteSB3aWxkZXN0IGRyZWFtcyEhCgpgYGB7cn0KdGFibGUodGVjLm5laWdoYm91ci5tZXJnZSRDbHVzdGVyLCB0ZWMubmVpZ2hib3VyLm1lcmdlJERpZmYpCmBgYAoKSSB3b3VsZCBzYXkgdGhhdCB0aGVzZSByZXN1bHRzIG1ha2UgYSBsb3Qgb2Ygc2Vuc2UuIEknbGwgZXh0ZW5kIGl0IHRvIGluY2x1ZGUgdGhlIHF1YWRyYXRpYyB0ZXN0aW5nIHdoaWNoIHNob3VsZCBwaWNrIHVwIHRoZSBpbnZlcnNlLXBhcmFib2xpYyBwcm9maWxlIG9mIHRoZSAKUG9zdC1BaXJlIG1URUMgcG9wdWxhdGlvbi4KCmBgYHtyfQpxdWFkLnRlYy5yZXMgPC0gYXMuZGF0YS5mcmFtZSh0b3BUYWdzKGdsbVFMRlRlc3QodGVjLmZpdCwgY29lZj0zKSwgc29ydC5ieT0nbm9uZScsIG49SW5mKSkKcXVhZC50ZWMucmVzJFNpZyA8LSBhcy5mYWN0b3IoYXMubnVtZXJpYyhxdWFkLnRlYy5yZXMkRkRSIDw9IDAuMDEpKQpxdWFkLnRlYy5yZXMkTmVpZ2hib3VyaG9vZCA8LSBhcy5udW1lcmljKHJvd25hbWVzKHF1YWQudGVjLnJlcykpCgojIGNvbnRyb2wgdGhlIHNwYXRpYWwgRkRSCnF2YWxzIDwtIHF1YWQudGVjLnJlcyRQVmFsdWUKaXMuc2lnIDwtIHF2YWxzIDw9IDAuMDEKc3VtbWFyeShpcy5zaWcpCmBgYAoKVGhlcmUgYXJlIDYgREEgbmVpZ2hib3VyaG9vZHMgZnJvbSB0aGUgcXVhZHJhdGljIG1vZGVsIC0gSSBleHBlY3QgdGhlc2Ugc2hvdWxkIHJlZmxlY3QgdGhlIFBvc3QtQWlyZSBtVEVDLiAKCmBgYHtyfQpxdWFkLnRlYy5zcGF0aWFsZmRyIDwtIGdyYXBoX3NwYXRpYWxGRFIobmVpZ2hib3Job29kcz10ZWMudmVydGV4Lmxpc3QsIGdyYXBoPXRlYy5rbm4sIGNvbm5lY3Rpdml0eT0iZGlzdGFuY2UiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBjYT1hcy5tYXRyaXgodGVjLnN1Yi5tZXRhWywgcGFzdGUwKCJQQyIsIDE6MzApXSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHZhbHVlcz1xdWFkLnRlYy5yZXNbb3JkZXIocXVhZC50ZWMucmVzJE5laWdoYm91cmhvb2QpLCBdJFBWYWx1ZSkKcXVhZC50ZWMucmVzJFNwYXRpYWxGRFJbb3JkZXIocXVhZC50ZWMucmVzJE5laWdoYm91cmhvb2QpXSA8LSBxdWFkLnRlYy5zcGF0aWFsZmRyCnF2YWxzIDwtIHF1YWQudGVjLnNwYXRpYWxmZHIKaXMuc2lnIDwtIHF2YWxzIDw9IDAuMDUKc3VtbWFyeShpcy5zaWcpCmBgYAoKRGFuZywgY2xlYXJseSB0aGUgc21hbGwgZ3JvdXAgb2YgUG9zdC1BaXJlIG1URUMgbWVhbnMgdGhpcyBpc24ndCBzdWZmaWNpZW50bHkgc2Vuc2l0aXZlIGFmdGVyIG11bHRpcGxlLXRlc3RpbmcgY29ycmVjdGlvbi4KCmBgYHtyLCBmaWcud2lkdGg9OS43NSwgZmlnLmhlaWdodD01LjE1fQpnZ3Bsb3QodGVjLm5laWdoYm91ci5tZXJnZSwgYWVzKHg9VU1BUDEsIHk9VU1BUDIpKSArCiAgZ2VvbV9wb2ludChkYXRhPXRlYy5uZWlnaGJvdXIubWVyZ2VbLCBjKCJVTUFQMSIsICJVTUFQMiIpXSwKICAgICAgICAgICAgIGNvbG91cj0nZ3JleTgwJywgc2l6ZT0xKSArCiAgZ2VvbV9wb2ludChkYXRhPXRlYy5uZWlnaGJvdXIubWVyZ2UsCiAgICAgICAgICAgICBhZXMoY29sb3VyPUNsdXN0ZXIpLCBzaXplPTQpICsKICB0aGVtZV9jbGVhbigpICsKICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcz1pbnRlci5jb2xzKSArCiAgZmFjZXRfd3JhcCh+Q2x1c3RlcikKYGBgCgpIbW0sIHRoZSBQb3N0LUFpcmUgbVRFQyBkb24ndCBmb3JtIGEgc2luZ2xlIG5laWdoYm91cmhvb2Qgb24gdGhlaXIgb3duLCBub3IgZG8gdGhlIG5URUMgb3IgVHVmdC1saWtlIG1URUMuIFRoZXJlIGlzIGRlZmluaXRlbHkgYSBsaW1pdCB0byB0aGUgcmVzb2x1dGlvbi4gV291bGQgYSAKc21hbGxlciAkayQgcmVzb2x2ZSB0aGlzPwoKIyMgQWdlaW5nIFRFQyB3aXRoIGs9MTEKCmBgYHtyfQp0ZWMuc3ViLm1ldGEgPC0gdGVjLm1ldGFbdGVjLm1ldGEkQWdlICVpbiUgYygiMXdrIiwgIjUyd2siKSwgXQoKIyBhZGQgdGhlIGxhYmVsIGFubm90YXRpb24KdGVjLnN1Yi5tZXRhJENsdXN0ZXIgPC0gIlVua25vd24iCnRlYy5zdWIubWV0YSRDbHVzdGVyW3RlYy5zdWIubWV0YSRURklERi5DbHVzdGVyID09ICIyIl0gPC0gIkludGVydHlwaWNhbCBURUMiCnRlYy5zdWIubWV0YSRDbHVzdGVyW3RlYy5zdWIubWV0YSRURklERi5DbHVzdGVyID09ICI5Il0gPC0gIlBlcmluYXRhbCBjVEVDIgp0ZWMuc3ViLm1ldGEkQ2x1c3Rlclt0ZWMuc3ViLm1ldGEkVEZJREYuQ2x1c3RlciA9PSAiMyJdIDwtICJNYXR1cmUgY1RFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjciXSA8LSAiTWF0dXJlIG1URUMiCnRlYy5zdWIubWV0YSRDbHVzdGVyW3RlYy5zdWIubWV0YSRURklERi5DbHVzdGVyID09ICIxIl0gPC0gIlBvc3QtQWlyZSBtVEVDIgp0ZWMuc3ViLm1ldGEkQ2x1c3Rlclt0ZWMuc3ViLm1ldGEkVEZJREYuQ2x1c3RlciA9PSAiNSJdIDwtICJUdWZ0LWxpa2UgbVRFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjYiXSA8LSAiUHJvbGlmZXJhdGluZyBURUMiCnRlYy5zdWIubWV0YSRDbHVzdGVyW3RlYy5zdWIubWV0YSRURklERi5DbHVzdGVyID09ICI4Il0gPC0gIm5URUMiCnRlYy5zdWIubWV0YSRDbHVzdGVyW3RlYy5zdWIubWV0YSRURklERi5DbHVzdGVyID09ICIxMCJdIDwtICJzVEVDIgoKaW50ZXIuY29scyA8LSBjKCIjOTk3MGFiIiwgIiMzNTk3OGYiLCAiI0IwY2RjMSIsICIjNzYyYTgzIiwgIiMwMTY2NWUiLCAiI2U3ZDRlOCIsICIjZGZjMjdkIiwgIiM4YzUxMGEiICwiI2JmODEyZCIpCm5hbWVzKGludGVyLmNvbHMpIDwtIGMoIlBvc3QtQWlyZSBtVEVDIiwgJ0ludGVydHlwaWNhbCBURUMnLCAnTWF0dXJlIGNURUMnLCAnVHVmdC1saWtlIG1URUMnLCAKICAgICAgICAgICAgICAgICAgICAgICAnUHJvbGlmZXJhdGluZyBURUMnLCAnTWF0dXJlIG1URUMnLCAnblRFQycsICdQZXJpbmF0YWwgY1RFQycsICdzVEVDJykKYGBgCgpJJ2xsIGJ1aWxkIGEga05OLWdyYXBoIGZyb20gdGhlIGZpcnN0IDMwIFBDcyBwcmV2aW91c2x5IGNvbXB1dGVkIG9uIGFsbCBURUMsIGJ1dCBrPTExIHRoaXMgdGltZS4KCmBgYHtyfQpzZXQuc2VlZCg0MikKdGVjLmtubiA8LSBidWlsZEtOTkdyYXBoKHg9YXMubWF0cml4KHRlYy5zdWIubWV0YVssIHBhc3RlMCgiUEMiLCAxOjMwKV0pLCBrPTExLCBkPU5BLCB0cmFuc3Bvc2VkPVRSVUUpCnRlYy5mci5sYXlvdXQgPC0gbGF5b3V0X3dpdGhfZnIodGVjLmtubikKcGxvdCh0ZWMua25uLCBsYXlvdXQ9dGVjLmZyLmxheW91dCwgdmVydGV4LmZyYW1lLmNvbG9yPSdza3libHVlJywgdmVydGV4LmNvbG9yPSdza3libHVlJywgdmVydGV4LmxhYmVsLmNvbG9yPSdibGFjaycsIAogICAgIHZlcnRleC5sYWJlbC5mYW1pbHk9J0hlbHZldGljYScsIGVkZ2UuY29sb3I9J2dyZXk2MCcsIHZlcnRleC5sYWJlbC5jZXg9MC45LAogICAgIHZlcnRleC5sYWJlbC5kaXN0PTEsIGVkZ2UuYXJyb3cuc2l6ZT0wLjIpCmBgYAoKVGhpcyBpcyBhIGZhaXJseSBkZW5zZWx5IGNvbm5lY3RlZCBuZXR3b3JrIHN0aWxsLCBldmVuIHdpdGggaz0xMSwgaG93IGRvZXMgdGhlIFVNQVAgbG9vaz8KCmBgYHtyfQpzZXQuc2VlZCg0MikKdGVjLnVtYXAgPC0gdW1hcChhcy5tYXRyaXgodGVjLnN1Yi5tZXRhWywgcGFzdGUwKCJQQyIsIDE6MzApXSksCiAgICAgICAgICAgICAgICAgbl9jb21wb25lbnRzPTIsCiAgICAgICAgICAgICAgICAgbl9uZWlnaGJvcnM9MTEsIG1ldHJpYz0nZXVjbGlkZWFuJywKICAgICAgICAgICAgICAgICBpbml0PSdyYW5kb20nLCBtaW5fZGlzdD0wLjIpCnRlYy51bWFwLmRmIDwtIGFzLmRhdGEuZnJhbWUodGVjLnVtYXAkbGF5b3V0KQpjb2xuYW1lcyh0ZWMudW1hcC5kZikgPC0gYygiVU1BUDEiLCAiVU1BUDIiKQp0ZWMudW1hcC5kZiRTYW1wbGUgPC0gdGVjLnN1Yi5tZXRhJFNhbXBsZQoKdGVjLnVtYXAubWVyZ2UgPC0gbWVyZ2UodGVjLnVtYXAuZGYsIHRlYy5zdWIubWV0YSwgYnk9J1NhbXBsZScpCmBgYAoKCmBgYHtyLCBmaWcud2lkdGg9Ny45NSwgZmlnLmhlaWdodD00LjE1fQpnZ3Bsb3QodGVjLnVtYXAubWVyZ2UsIGFlcyh4PVVNQVAxLCB5PVVNQVAyKSkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG91cj1DbHVzdGVyKSkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzPWludGVyLmNvbHMpICsKICBmYWNldF93cmFwKH5BZ2UpICsKICBndWlkZXMoY29sb3VyPWd1aWRlX2xlZ2VuZChvdmVycmlkZS5hZXM9bGlzdChzaXplPTMpKSwKICAgICAgICAgc2hhcGU9Z3VpZGVfbGVnZW5kKG92ZXJyaWRlLmFlcz1saXN0KHNpemU9MykpKQpgYGAKCkZvciBhIHNtYWxsZXIgaywgZG9lcyB0aGVyZSBuZWVkIHRvIGJlIGEgaGlnaGVyIGRlbnNpdHkgb2Ygc2FtcGxpbmcsIGkuZS4gbW9yZSBuZWlnaGJvdXJob29kcz8gSSdsbCBzZXQgaXQgdG8gMTAlIGhlcmUgaW5zdGVhZC4KCmBgYHtyfQojIHJhbmRvbWx5IHNlbGVjdCB2ZXJ0aWNlcyBpbiB0aGUgZ3JhcGgKbi5ob29kIDwtIDAuMTAKdGVjLnJhbmRvbS52ZXJ0aWNlcyA8LSBzYW1wbGUoVih0ZWMua25uKSwgc2l6ZT1mbG9vcihuLmhvb2QqbGVuZ3RoKFYodGVjLmtubikpKSkKIyBsb29wIG92ZXIgcmFuZG9tIHZlcnRpY2VzIGFuZCBjb3VudCB0aGUgbnVtYmVyIG9mIGNlbGxzIGluIGVhY2gKdGVjLnZlcnRleC5saXN0IDwtIHNhcHBseSgxOmxlbmd0aCh0ZWMucmFuZG9tLnZlcnRpY2VzKSwgRlVOPWZ1bmN0aW9uKFgpIG5laWdoYm9ycyh0ZWMua25uLCB2PXRlYy5yYW5kb20udmVydGljZXNbWF0pKQpoaXN0KHVubGlzdChsYXBwbHkodGVjLnZlcnRleC5saXN0LCBsZW5ndGgpKSwgMTAwLCBtYWluPSJIaXN0b2dyYW0gb2YgbmVpZ2hib3JzIiwgeGxhYj0iTmVpZ2hib3VyaG9vZCBzaXplIikKYGBgCgpUaGlzIGlzIHRoZSBoaXN0b2dyYW0gb2YgVEVDIG5laWdoYm91cmhvb2Qgc2l6ZXMuCgpgYGB7cn0KdGVjLnVtYXAubWVyZ2UkRXhwU2FtcCA8LSBwYXN0ZSh0ZWMudW1hcC5tZXJnZSRBZ2UsIHRlYy51bWFwLm1lcmdlJFNvcnREYXksIHNlcD0iXyIpCnRlYy51bWFwLm1lcmdlJFZlcnRleCA8LSBjKDE6bnJvdyh0ZWMudW1hcC5tZXJnZSkpCnRlYy5jb3VudHMgPC0gcXVhbnRfbmVpZ2hib3VyaG9vZChncmFwaD10ZWMua25uLCBtZXRhPXRlYy51bWFwLm1lcmdlLCBzYW1wbGUuY29sdW1uPSdFeHBTYW1wJywgc2FtcGxlLnZlcnRpY2VzPW4uaG9vZCkKYGBgCgoKYGBge3J9CnRlYy5yZXBzIDwtIHVubGlzdChsYXBwbHkoc3Ryc3BsaXQodW5pcXVlKHRlYy51bWFwLm1lcmdlJEV4cFNhbXApLCBzcGxpdD0iXyIpLCBGVU49ZnVuY3Rpb24oWCkgcGFzdGUwKFhbMl0pKSkKdGVjLmNvbmQgPC0gdW5saXN0KGxhcHBseShzdHJzcGxpdCh1bmlxdWUodGVjLnVtYXAubWVyZ2UkRXhwU2FtcCksIHNwbGl0PSJfIiksIEZVTj1mdW5jdGlvbihYKSBwYXN0ZTAoWFsxXSkpKQoKdGVjLnNhbXBsZS5tZXRhIDwtIGRhdGEuZnJhbWUoIkNvbmRpdGlvbiI9dGVjLmNvbmQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJSZXBsaWNhdGUiPXRlYy5yZXBzKQp0ZWMuc2FtcGxlLm1ldGEkU2FtcGxlIDwtIHBhc3RlKHRlYy5zYW1wbGUubWV0YSRDb25kaXRpb24sIHRlYy5zYW1wbGUubWV0YSRSZXBsaWNhdGUsIHNlcD0iXyIpCnJvd25hbWVzKHRlYy5zYW1wbGUubWV0YSkgPC0gdGVjLnNhbXBsZS5tZXRhJFNhbXBsZQoKdGVjLm1vZGVsIDwtIG1vZGVsLm1hdHJpeCh+IENvbmRpdGlvbiwgZGF0YT10ZWMuc2FtcGxlLm1ldGEpCmhlYWQodGVjLm1vZGVsKQpgYGAKCmBgYHtyfQp0ZWMuZGdlIDwtIERHRUxpc3QodGVjLmNvdW50c1ssIHJvd25hbWVzKHRlYy5tb2RlbCldLCBsaWIuc2l6ZT1sb2coY29sU3Vtcyh0ZWMuY291bnRzWywgcm93bmFtZXModGVjLm1vZGVsKV0pKSkKdGVjLmRnZSA8LSBlc3RpbWF0ZURpc3AodGVjLmRnZSwgdGVjLm1vZGVsKQp0ZWMuZml0IDwtIGdsbVFMRml0KHRlYy5kZ2UsIHRlYy5tb2RlbCwgcm9idXN0PVRSVUUpCiMgdGVjLmNvbnRyYXN0IDwtIG1ha2VDb250cmFzdHMoQ29uZGl0aW9uQSAtIENvbmRpdGlvbkIsIGxldmVscz10ZWMubW9kZWwpCiMgdGVjLnJlcyA8LSBnbG1RTEZUZXN0KHRlYy5maXQsIGNvbnRyYXN0PXRlYy5jb250cmFzdCkKCnRlYy5yZXMgPC0gYXMuZGF0YS5mcmFtZSh0b3BUYWdzKGdsbVFMRlRlc3QodGVjLmZpdCwgY29lZj0yKSwgc29ydC5ieT0nbm9uZScsIG49SW5mKSkKdGVjLnJlcyRTaWcgPC0gYXMuZmFjdG9yKGFzLm51bWVyaWModGVjLnJlcyRGRFIgPD0gMC4wNSkpCnRlYy5yZXMkTmVpZ2hib3VyaG9vZCA8LSBhcy5udW1lcmljKHJvd25hbWVzKHRlYy5yZXMpKQoKIyBjb250cm9sIHRoZSBzcGF0aWFsIEZEUgpxdmFscyA8LSB0ZWMucmVzJFBWYWx1ZQppcy5zaWcgPC0gcXZhbHMgPD0gMC4wNQpzdW1tYXJ5KGlzLnNpZykKYGBgCgpJIGhhdmUgaW5jcmVhc2VkIHRoZSB0b3RhbCBudW1iZXIgb2YgbmVpZ2hib3VyaG9vZHMgZGVmaW5lZCB3aXRoIGs9MTEsIHRoZXJlIHdpbGwgYWxtb3N0IGNlcnRhaW5seSBiZSBhIHRyYWRlLW9mZiBiZXR3ZWVuIHNlbnNpdGl2aXR5IGFuZCBwb3dlciB3LnIudC4gdGhlIGV4dHJhIAptdWx0aXBsZS10ZXN0aW5nIGJ1cmRlbiwgYXMgd2VsbCBhcyB0aGUgaGlnaGVyIHNhbXBsaW5nIHZhcmlhbmNlIGFzIGVhY2ggbmVpZ2hib3VyaG9vZCB3aWxsIGNvbnRhaW4gZmV3ZXIgY2VsbHMgPC0gdGhpcyB3aWxsIGJlIHNvbWV0aGluZyB3ZSBuZWVkIHRvIG9wdGltaXNlIGluIApzb21lIHdheS4KCmBgYHtyfQp0ZWMuc3BhdGlhbGZkciA8LSBncmFwaF9zcGF0aWFsRkRSKG5laWdoYm9yaG9vZHM9dGVjLnZlcnRleC5saXN0LCBncmFwaD10ZWMua25uLCBjb25uZWN0aXZpdHk9ImRpc3RhbmNlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwY2E9YXMubWF0cml4KHRlYy5zdWIubWV0YVssIHBhc3RlMCgiUEMiLCAxOjMwKV0pLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHB2YWx1ZXM9dGVjLnJlc1tvcmRlcih0ZWMucmVzJE5laWdoYm91cmhvb2QpLCBdJFBWYWx1ZSkKdGVjLnJlcyRTcGF0aWFsRkRSW29yZGVyKHRlYy5yZXMkTmVpZ2hib3VyaG9vZCldIDwtIHRlYy5zcGF0aWFsZmRyCnF2YWxzIDwtIHRlYy5zcGF0aWFsZmRyCmlzLnNpZyA8LSBxdmFscyA8PSAwLjA1CnN1bW1hcnkoaXMuc2lnKQpgYGAKCkludGVyZXN0aW5nLCAxMSBvZiB0aGUgbmVpZ2hib3VyaG9vZHMgYXJlIG5vIGxvbmcgc3RhdGlzdGljYWxseSBzaWduaWZpY2FudCBhZnRlciB0aGUgc3BhdGlhbCBGRFIgY29ycmVjdGlvbi4gQ2xlYXJseSB0aGVyZSBpcyBhIHRyYWRlLW9mZiBiZXR3ZWVuIG5laWdoYm91cmhvb2QgCnNpemUsIHNlbnNpdGl2aXR5IGFuZCBwb3dlci4KCkluIGVhY2ggbmVpZ2hib3VyaG9vZCwgd2hhdCBpcyB0aGUgbW9zdCBjb21tb24gY29uZGl0aW9uIG9yIGJsb2NrIG9mIGNlbGxzPwoKYGBge3J9CnRlYy5uZWlnaGJvdXIuZXhwcnMgPC0gbmVpZ2hib3Job29kX2V4cHJlc3Npb24odGVjLnZlcnRleC5saXN0LCB0ZWMuc3ViLmdleCkKYGBgCgpFbWJlZCB0aGVzZSBoeXBlcnNwaGVyZXMgd2l0aCBhIFBDQSBhbmQgVU1BUC4KCmBgYHtyfQp0ZWMubmVpZ2hib3VyLnBjYSA8LSBwcmNvbXAoKHQodGVjLm5laWdoYm91ci5leHByc1t0ZWMuaHZncyRIVkcsIF0pKSkKcGFpcnModGVjLm5laWdoYm91ci5wY2EkeFssIGMoMTo1KV0pCmBgYAoKYGBge3J9CnNldC5zZWVkKDQyKQpuZWlnaGJvdXJob29kLnVtYXAgPC0gdW1hcCh0ZWMubmVpZ2hib3VyLnBjYSR4WywgYygxOjMwKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG5fY29tcG9uZW50cz0yLAogICAgICAgICAgICAgICAgICAgICAgICAgICBuX25laWdoYm9ycz0xMSwgbWV0cmljPSdldWNsaWRlYW4nLAogICAgICAgICAgICAgICAgICAgICAgICAgICBpbml0PSdyYW5kb20nLCBtaW5fZGlzdD0wLjEpCmBgYAoKV2UgY2FuIG92ZXJsYXkgdGhlIERBIHRlc3Rpbmcgb24gdGhlc2UgbmVpZ2hib3VyaG9vZHMuCgpgYGB7cn0KdGVjLm5laWdoYm9yLmRmIDwtIHRlYy5yZXNbLCBjKCJsb2dGQyIsICJOZWlnaGJvdXJob29kIiwgIlNwYXRpYWxGRFIiKV0KdGVjLm5laWdoYm9yLmRmIDwtIGRvLmNhbGwoY2JpbmQuZGF0YS5mcmFtZSwgbGlzdCh0ZWMubmVpZ2hib3IuZGYsIGFzLmRhdGEuZnJhbWUobmVpZ2hib3VyaG9vZC51bWFwJGxheW91dCkpKQpjb2xuYW1lcyh0ZWMubmVpZ2hib3IuZGYpIDwtIGMoImxvZ0ZDIiwgIk5laWdoYm91cmhvb2QiLCAiU3BhdGlhbEZEUiIsICJVTUFQMSIsICJVTUFQMiIpCnRlYy5uZWlnaGJvci5kZiRTaWcgPC0gYXMubnVtZXJpYyh0ZWMubmVpZ2hib3IuZGYkU3BhdGlhbEZEUiA8PSAwLjA1KQoKZ2dwbG90KHRlYy5uZWlnaGJvci5kZiwgYWVzKHg9VU1BUDEsIHk9VU1BUDIpKSArCiAgZ2VvbV9wb2ludChkYXRhPXRlYy5uZWlnaGJvci5kZlt0ZWMubmVpZ2hib3IuZGYkU2lnID09IDAsIF0sCiAgICAgICAgICAgICBjb2xvdXI9J2dyZXk4MCcsIHNpemU9MikgKwogIGdlb21fcG9pbnQoZGF0YT10ZWMubmVpZ2hib3IuZGZbdGVjLm5laWdoYm9yLmRmJFNpZyA9PSAxLCBdLAogICAgICAgICAgICAgYWVzKGNvbG91cj1sb2dGQyksIHNpemU9NCkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9ncmFkaWVudDIobG93PSJibHVlIiwgbWlkPSJncmV5ODAiLCBoaWdoPSJyZWQiKQpgYGAKClNvbWUgYXJlIHVwIGFuZCBzb21lIGFyZSBkb3duLiBXaGljaCBURUMgc3VidHlwZXMgZG8gdGhleSBsYXJnZWx5IGNvcnJlc3BvbmQgd2l0aD8KCmBgYHtyfQp0ZWMubmVpZ2hib3VyLmxpc3QgPC0gbGlzdCgpCmZvcih4IGluIHNlcV9hbG9uZygxOmxlbmd0aCh0ZWMudmVydGV4Lmxpc3QpKSl7CiAgeC5kZiA8LSB0ZWMudW1hcC5tZXJnZVt0ZWMudW1hcC5tZXJnZSRWZXJ0ZXggJWluJSB0ZWMudmVydGV4Lmxpc3RbW3hdXSwgXQogIHgucmVwIDwtIG5hbWVzKHRhYmxlKHguZGYkU29ydERheSkpW3doaWNoKHRhYmxlKHguZGYkU29ydERheSkgPT0gbWF4KHRhYmxlKHguZGYkU29ydERheSkpKV0KICBpZihsZW5ndGgoeC5yZXApID4gMSl7CiAgICB4LnJlcCA8LSBzYW1wbGUoc2l6ZT0xLCB4LnJlcCkKICB9CiAgeC5ibG9jayA8LSBuYW1lcyh0YWJsZSh4LmRmJENsdXN0ZXIpKVt3aGljaCh0YWJsZSh4LmRmJENsdXN0ZXIpID09IG1heCh0YWJsZSh4LmRmJENsdXN0ZXIpKSldCiAgICBpZihsZW5ndGgoeC5ibG9jaykgPiAxKXsKICAgIHguYmxvY2sgPC0gc2FtcGxlKHNpemU9MSwgeC5ibG9jaykKICB9CiAgeC5jb25kaXRpb24gPC0gbmFtZXModGFibGUoeC5kZiRBZ2UpKVt3aGljaCh0YWJsZSh4LmRmJEFnZSkgPT0gbWF4KHRhYmxlKHguZGYkQWdlKSkpXQogICAgaWYobGVuZ3RoKHguY29uZGl0aW9uKSA+IDEpewogICAgeC5jb25kaXRpb24gPC0gc2FtcGxlKHNpemU9MSwgeC5jb25kaXRpb24pCiAgfQogIAogIHRlYy5uZWlnaGJvdXIubGlzdFtbeF1dIDwtIGRhdGEuZnJhbWUoIlJlcGxpY2F0ZSI9eC5yZXAsICJDbHVzdGVyIj14LmJsb2NrLCAiQ29uZGl0aW9uIj14LmNvbmRpdGlvbiwgIk5laWdoYm91cmhvb2QiPXgpCn0KCnRlYy5uZWlnaGJvdXIubWV0YSA8LSBkby5jYWxsKHJiaW5kLmRhdGEuZnJhbWUsIHRlYy5uZWlnaGJvdXIubGlzdCkKdGVjLm5laWdoYm91ci5tZXJnZSA8LSBtZXJnZSh0ZWMubmVpZ2hib3IuZGYsIHRlYy5uZWlnaGJvdXIubWV0YSwgYnk9J05laWdoYm91cmhvb2QnKQoKdGVjLm5laWdoYm91ci5tZXJnZSREaWZmIDwtIHNpZ24odGVjLm5laWdoYm91ci5tZXJnZSRsb2dGQykKdGVjLm5laWdoYm91ci5tZXJnZSREaWZmW3RlYy5uZWlnaGJvdXIubWVyZ2UkU2lnID09IDBdIDwtIDAKYGBgCgoKYGBge3IsIGZpZy53aWR0aD05Ljc1LCBmaWcuaGVpZ2h0PTQuMTV9CmdncGxvdCh0ZWMubmVpZ2hib3VyLm1lcmdlLCBhZXMoeD1VTUFQMSwgeT1VTUFQMikpICsKICBnZW9tX3BvaW50KGRhdGE9dGVjLm5laWdoYm91ci5tZXJnZVssIGMoIlVNQVAxIiwgIlVNQVAyIildLAogICAgICAgICAgICAgY29sb3VyPSdncmV5ODAnLCBzaXplPTEpICsKICBnZW9tX3BvaW50KGRhdGE9dGVjLm5laWdoYm91ci5tZXJnZVt0ZWMubmVpZ2hib3VyLm1lcmdlJFNpZyA9PSAxLCBdLAogICAgICAgICAgICAgYWVzKGNvbG91cj1sb2dGQyksIHNpemU9NCkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9ncmFkaWVudDIobG93PSJibHVlIiwgbWlkPSJncmV5ODAiLCBoaWdoPSJyZWQiKSArCiAgZmFjZXRfd3JhcCh+Q2x1c3RlcikKYGBgCgpUaGVyZSBpcyBhIHN1YnNldCBvZiB0aGUgUHJvbGlmZXJhdGluZyBURUMgdGhhdCBhcmUgZG93biwgZ29vZCwgYXMgYXJlIHRoZSBQZXJpbmF0YWwgY1RFQy4gTGlrZXdpc2UsIG1vc3Qgb2YgdGhlIEludGVydHlwaWNhbCBURUMgYXJlIHVwIGFzIHdlbGwsIGJ1dCBzb21lIGFyZSAKYWxzbyBkb3duLiBJIGRvbid0IGtub3cgaWYgdGhpcyBpcyBiZWNhdXNlIG9mIGEgY29tcG9zaXRpb25hbCBlZmZlY3Qgb3IgYmVjYXVzZSB0aGVzZSBhcmUgdGhlIG9uZXMgdGhhdCB3b3VsZCBiZSBkaWZmZXJlbnRpYXRpbmcgaW50byBtVEVDIHZpYSB0aGUgUHJvbGlmZXJhdGluZyAKVEVDIGNvbXBhcnRtZW50LgoKYGBge3J9CnRhYmxlKHRlYy5uZWlnaGJvdXIubWVyZ2UkQ2x1c3RlciwgdGVjLm5laWdoYm91ci5tZXJnZSREaWZmKQpgYGAKCkkgd291bGQgc2F5IHRoYXQgdGhlc2UgcmVzdWx0cyBtYWtlIGEgbG90IG9mIHNlbnNlLiBDYW4gSSBub3cgZXh0ZW5kIHRoaXMgdG8gaW5jbHVkZSBhbGwgdGltZSBwb2ludHMgYW5kIGZpdCBhZ2UgYXMgYSBsaW5lYXIgb3JkaW5hbCB2YXJpYWJsZT8KCiMjIEV4dGVuZGVkIFRFQyBEQSB0ZXN0aW5nIHdpdGggaz0xMQoKYGBge3J9CiMgZXhjbHVkZSB0ZWNobmljYWwgYXJ0aWZhY3QgY2x1c3Rlcgp0ZWMuc3ViLm1ldGEgPC0gdGVjLm1ldGEKdGVjLnN1Yi5tZXRhJEFnZUZhY3RvciA8LSBvcmRlcmVkKHRlYy5zdWIubWV0YSRBZ2UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHM9YygiMXdrIiwgIjR3ayIsICIxNndrIiwgIjMyd2siLCAiNTJ3ayIpKQojIGFkZCB0aGUgbGFiZWwgYW5ub3RhdGlvbgp0ZWMuc3ViLm1ldGEkQ2x1c3RlciA8LSAiVW5rbm93biIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjIiXSA8LSAiSW50ZXJ0eXBpY2FsIFRFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjkiXSA8LSAiUGVyaW5hdGFsIGNURUMiCnRlYy5zdWIubWV0YSRDbHVzdGVyW3RlYy5zdWIubWV0YSRURklERi5DbHVzdGVyID09ICIzIl0gPC0gIk1hdHVyZSBjVEVDIgp0ZWMuc3ViLm1ldGEkQ2x1c3Rlclt0ZWMuc3ViLm1ldGEkVEZJREYuQ2x1c3RlciA9PSAiNyJdIDwtICJNYXR1cmUgbVRFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjEiXSA8LSAiUG9zdC1BaXJlIG1URUMiCnRlYy5zdWIubWV0YSRDbHVzdGVyW3RlYy5zdWIubWV0YSRURklERi5DbHVzdGVyID09ICI1Il0gPC0gIlR1ZnQtbGlrZSBtVEVDIgp0ZWMuc3ViLm1ldGEkQ2x1c3Rlclt0ZWMuc3ViLm1ldGEkVEZJREYuQ2x1c3RlciA9PSAiNiJdIDwtICJQcm9saWZlcmF0aW5nIFRFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjgiXSA8LSAiblRFQyIKdGVjLnN1Yi5tZXRhJENsdXN0ZXJbdGVjLnN1Yi5tZXRhJFRGSURGLkNsdXN0ZXIgPT0gIjEwIl0gPC0gInNURUMiCgppbnRlci5jb2xzIDwtIGMoIiM5OTcwYWIiLCAiIzM1OTc4ZiIsICIjQjBjZGMxIiwgIiM3NjJhODMiLCAiIzAxNjY1ZSIsICIjZTdkNGU4IiwgIiNkZmMyN2QiLCAiIzhjNTEwYSIgLCIjYmY4MTJkIikKbmFtZXMoaW50ZXIuY29scykgPC0gYygiUG9zdC1BaXJlIG1URUMiLCAnSW50ZXJ0eXBpY2FsIFRFQycsICdNYXR1cmUgY1RFQycsICdUdWZ0LWxpa2UgbVRFQycsIAogICAgICAgICAgICAgICAgICAgICAgICdQcm9saWZlcmF0aW5nIFRFQycsICdNYXR1cmUgbVRFQycsICduVEVDJywgJ1BlcmluYXRhbCBjVEVDJywgJ3NURUMnKQpgYGAKCmBgYHtyfQp0ZWMuc3ViLmdleCA8LSB0ZWMuZ2V4WywgY29sbmFtZXModGVjLmdleCkgJWluJSB0ZWMuc3ViLm1ldGEkU2FtcGxlXQpgYGAKCgpJJ2xsIGJ1aWxkIGEga05OLWdyYXBoIGZyb20gdGhlIGZpcnN0IDMwIFBDcyBwcmV2aW91c2x5IGNvbXB1dGVkIG9uIGFsbCBURUMuCgpgYGB7cn0Kc2V0LnNlZWQoNDIpCnRlYy5rbm4gPC0gYnVpbGRLTk5HcmFwaCh4PWFzLm1hdHJpeCh0ZWMuc3ViLm1ldGFbLCBwYXN0ZTAoIlBDIiwgMTozMCldKSwgaz0xMSwgZD1OQSwgdHJhbnNwb3NlZD1UUlVFKQp0ZWMuZnIubGF5b3V0IDwtIGxheW91dF93aXRoX2ZyKHRlYy5rbm4pCnBsb3QodGVjLmtubiwgbGF5b3V0PXRlYy5mci5sYXlvdXQsIHZlcnRleC5mcmFtZS5jb2xvcj0nc2t5Ymx1ZScsIHZlcnRleC5jb2xvcj0nc2t5Ymx1ZScsIHZlcnRleC5sYWJlbC5jb2xvcj0nYmxhY2snLCAKICAgICB2ZXJ0ZXgubGFiZWwuZmFtaWx5PSdIZWx2ZXRpY2EnLCBlZGdlLmNvbG9yPSdncmV5NjAnLCB2ZXJ0ZXgubGFiZWwuY2V4PTAuOSwKICAgICB2ZXJ0ZXgubGFiZWwuZGlzdD0xLCBlZGdlLmFycm93LnNpemU9MC4yKQpgYGAKClRoaXMgaXMgYSBmYWlybHkgZGVuc2VseSBjb25uZWN0ZWQgbmV0d29yaywgaG93IGRvZXMgdGhlIFVNQVAgbG9vaz8KCmBgYHtyfQpzZXQuc2VlZCg0MikKdGVjLnVtYXAgPC0gdW1hcChhcy5tYXRyaXgodGVjLnN1Yi5tZXRhWywgcGFzdGUwKCJQQyIsIDE6MzApXSksCiAgICAgICAgICAgICAgICAgbl9jb21wb25lbnRzPTIsCiAgICAgICAgICAgICAgICAgbl9uZWlnaGJvcnM9MTEsIG1ldHJpYz0nZXVjbGlkZWFuJywKICAgICAgICAgICAgICAgICBpbml0PSdyYW5kb20nLCBtaW5fZGlzdD0wLjIpCnRlYy51bWFwLmRmIDwtIGFzLmRhdGEuZnJhbWUodGVjLnVtYXAkbGF5b3V0KQpjb2xuYW1lcyh0ZWMudW1hcC5kZikgPC0gYygiVU1BUDEiLCAiVU1BUDIiKQp0ZWMudW1hcC5kZiRTYW1wbGUgPC0gdGVjLnN1Yi5tZXRhJFNhbXBsZQoKdGVjLnVtYXAubWVyZ2UgPC0gbWVyZ2UodGVjLnVtYXAuZGYsIHRlYy5zdWIubWV0YSwgYnk9J1NhbXBsZScpCmBgYAoKCmBgYHtyLCBmaWcud2lkdGg9OS45NSwgZmlnLmhlaWdodD03LjE1fQpnZ3Bsb3QodGVjLnVtYXAubWVyZ2UsIGFlcyh4PVVNQVAxLCB5PVVNQVAyKSkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG91cj1DbHVzdGVyKSkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzPWludGVyLmNvbHMpICsKICBmYWNldF93cmFwKH5BZ2VGYWN0b3IpICsKICBndWlkZXMoY29sb3VyPWd1aWRlX2xlZ2VuZChvdmVycmlkZS5hZXM9bGlzdChzaXplPTMpKSwKICAgICAgICAgc2hhcGU9Z3VpZGVfbGVnZW5kKG92ZXJyaWRlLmFlcz1saXN0KHNpemU9MykpKQpgYGAKCgpgYGB7cn0KIyByYW5kb21seSBzZWxlY3QgdmVydGljZXMgaW4gdGhlIGdyYXBoCm4uaG9vZCA8LSAwLjEwCnRlYy5yYW5kb20udmVydGljZXMgPC0gc2FtcGxlKFYodGVjLmtubiksIHNpemU9Zmxvb3Iobi5ob29kKmxlbmd0aChWKHRlYy5rbm4pKSkpCiMgbG9vcCBvdmVyIHJhbmRvbSB2ZXJ0aWNlcyBhbmQgY291bnQgdGhlIG51bWJlciBvZiBjZWxscyBpbiBlYWNoCnRlYy52ZXJ0ZXgubGlzdCA8LSBzYXBwbHkoMTpsZW5ndGgodGVjLnJhbmRvbS52ZXJ0aWNlcyksIEZVTj1mdW5jdGlvbihYKSBuZWlnaGJvcnModGVjLmtubiwgdj10ZWMucmFuZG9tLnZlcnRpY2VzW1hdKSkKaGlzdCh1bmxpc3QobGFwcGx5KHRlYy52ZXJ0ZXgubGlzdCwgbGVuZ3RoKSksIDEwMCwgbWFpbj0iSGlzdG9ncmFtIG9mIG5laWdoYm9ycyIsIHhsYWI9Ik5laWdoYm91cmhvb2Qgc2l6ZSIpCmBgYAoKVGhpcyBpcyB0aGUgaGlzdG9ncmFtIG9mIFRFQyBuZWlnaGJvdXJob29kIHNpemVzLgoKYGBge3J9CnRlYy51bWFwLm1lcmdlJEV4cFNhbXAgPC0gcGFzdGUodGVjLnVtYXAubWVyZ2UkQWdlLCB0ZWMudW1hcC5tZXJnZSRTb3J0RGF5LCBzZXA9Il8iKQp0ZWMudW1hcC5tZXJnZSRWZXJ0ZXggPC0gYygxOm5yb3codGVjLnVtYXAubWVyZ2UpKQp0ZWMuY291bnRzIDwtIHF1YW50X25laWdoYm91cmhvb2QoZ3JhcGg9dGVjLmtubiwgbWV0YT10ZWMudW1hcC5tZXJnZSwgc2FtcGxlLmNvbHVtbj0nRXhwU2FtcCcsIHNhbXBsZS52ZXJ0aWNlcz1uLmhvb2QpCmBgYAoKCmBgYHtyfQp0ZWMucmVwcyA8LSB1bmxpc3QobGFwcGx5KHN0cnNwbGl0KHVuaXF1ZSh0ZWMudW1hcC5tZXJnZSRFeHBTYW1wKSwgc3BsaXQ9Il8iKSwgRlVOPWZ1bmN0aW9uKFgpIHBhc3RlMChYWzJdKSkpCnRlYy5jb25kIDwtIHVubGlzdChsYXBwbHkoc3Ryc3BsaXQodW5pcXVlKHRlYy51bWFwLm1lcmdlJEV4cFNhbXApLCBzcGxpdD0iXyIpLCBGVU49ZnVuY3Rpb24oWCkgcGFzdGUwKFhbMV0pKSkKCnRlYy5zYW1wbGUubWV0YSA8LSBkYXRhLmZyYW1lKCJDb25kaXRpb24iPXRlYy5jb25kLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiUmVwbGljYXRlIj10ZWMucmVwcykKdGVjLnNhbXBsZS5tZXRhJFNhbXBsZSA8LSBwYXN0ZSh0ZWMuc2FtcGxlLm1ldGEkQ29uZGl0aW9uLCB0ZWMuc2FtcGxlLm1ldGEkUmVwbGljYXRlLCBzZXA9Il8iKQpyb3duYW1lcyh0ZWMuc2FtcGxlLm1ldGEpIDwtIHRlYy5zYW1wbGUubWV0YSRTYW1wbGUKdGVjLnNhbXBsZS5tZXRhJENvbmRpdGlvbiA8LSBvcmRlcmVkKHRlYy5zYW1wbGUubWV0YSRDb25kaXRpb24sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHM9YygiMXdrIiwgIjR3ayIsICIxNndrIiwgIjMyd2siLCAiNTJ3ayIpKQp0ZWMubW9kZWwgPC0gbW9kZWwubWF0cml4KH4gQ29uZGl0aW9uLCBkYXRhPXRlYy5zYW1wbGUubWV0YSkKaGVhZCh0ZWMubW9kZWwpCmBgYAoKYGBge3J9CnRlYy5kZ2UgPC0gREdFTGlzdCh0ZWMuY291bnRzWywgcm93bmFtZXModGVjLm1vZGVsKV0sIGxpYi5zaXplPWxvZyhjb2xTdW1zKHRlYy5jb3VudHNbLCByb3duYW1lcyh0ZWMubW9kZWwpXSkpKQp0ZWMuZGdlIDwtIGVzdGltYXRlRGlzcCh0ZWMuZGdlLCB0ZWMubW9kZWwpCnRlYy5maXQgPC0gZ2xtUUxGaXQodGVjLmRnZSwgdGVjLm1vZGVsLCByb2J1c3Q9VFJVRSkKIyB0ZWMuY29udHJhc3QgPC0gbWFrZUNvbnRyYXN0cyhDb25kaXRpb25BIC0gQ29uZGl0aW9uQiwgbGV2ZWxzPXRlYy5tb2RlbCkKIyB0ZWMucmVzIDwtIGdsbVFMRlRlc3QodGVjLmZpdCwgY29udHJhc3Q9dGVjLmNvbnRyYXN0KQoKdGVjLnJlcyA8LSBhcy5kYXRhLmZyYW1lKHRvcFRhZ3MoZ2xtUUxGVGVzdCh0ZWMuZml0LCBjb2VmPTIpLCBzb3J0LmJ5PSdub25lJywgbj1JbmYpKQp0ZWMucmVzJFNpZyA8LSBhcy5mYWN0b3IoYXMubnVtZXJpYyh0ZWMucmVzJEZEUiA8PSAwLjA1KSkKdGVjLnJlcyROZWlnaGJvdXJob29kIDwtIGFzLm51bWVyaWMocm93bmFtZXModGVjLnJlcykpCgojIGNvbnRyb2wgdGhlIHNwYXRpYWwgRkRSCnF2YWxzIDwtIHRlYy5yZXMkUFZhbHVlCmlzLnNpZyA8LSBxdmFscyA8PSAwLjA1CnN1bW1hcnkoaXMuc2lnKQpgYGAKClRoZXJlIGFyZSA4NSBEQSBuZWlnaGJvdXJob29kcyAtIEkgZXhwZWN0IHRoZXNlIHNob3VsZCByZWZsZWN0IHRoZSBQZXJpbmF0YWwsIEludGVydHlwaWNhbCwgUHJvbGlmZXJhdGluZyBhbmQgc1RFQy4gSSdsbCB1c2UgdGhlIGRpc3RhbmNlLWJhc2VkIGFwcHJvYWNoIHRvIGNvcnJlY3QgZm9yIAp0aGUgc3BhdGlhbCBGRFIuCgpgYGB7cn0KdGVjLnNwYXRpYWxmZHIgPC0gZ3JhcGhfc3BhdGlhbEZEUihuZWlnaGJvcmhvb2RzPXRlYy52ZXJ0ZXgubGlzdCwgZ3JhcGg9dGVjLmtubiwgY29ubmVjdGl2aXR5PSJkaXN0YW5jZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGNhPWFzLm1hdHJpeCh0ZWMuc3ViLm1ldGFbLCBwYXN0ZTAoIlBDIiwgMTozMCldKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwdmFsdWVzPXRlYy5yZXNbb3JkZXIodGVjLnJlcyROZWlnaGJvdXJob29kKSwgXSRQVmFsdWUpCnRlYy5yZXMkU3BhdGlhbEZEUltvcmRlcih0ZWMucmVzJE5laWdoYm91cmhvb2QpXSA8LSB0ZWMuc3BhdGlhbGZkcgpxdmFscyA8LSB0ZWMuc3BhdGlhbGZkcgppcy5zaWcgPC0gcXZhbHMgPD0gMC4wNQpzdW1tYXJ5KGlzLnNpZykKYGBgCgpJbnRlcmVzdGluZywgdGhlcmUgYXJlIDI2IG5laWdoYm91cmhvb2RzIG5vIGxvbmdlciBzdGF0aXN0aWNhbGx5IHNpZ25pZmljYW50IGFmdGVyIHRoZSBzcGF0aWFsIEZEUiBjb3JyZWN0aW9uIC0gaG9wZWZ1bGx5IHRoZXNlIGFyZSBnZW51aW5lbHkgZmFsc2UtcG9zaXRpdmVzLgoKSW4gZWFjaCBuZWlnaGJvdXJob29kLCB3aGF0IGlzIHRoZSBtb3N0IGNvbW1vbiBjb25kaXRpb24gb3IgYmxvY2sgb2YgY2VsbHM/CgpgYGB7cn0KdGVjLm5laWdoYm91ci5leHBycyA8LSBuZWlnaGJvcmhvb2RfZXhwcmVzc2lvbih0ZWMudmVydGV4Lmxpc3QsIHRlYy5zdWIuZ2V4KQpgYGAKCkVtYmVkIHRoZXNlIGh5cGVyc3BoZXJlcyB3aXRoIGEgUENBIGFuZCBVTUFQLgoKYGBge3J9CnRlYy5uZWlnaGJvdXIucGNhIDwtIHByY29tcCgodCh0ZWMubmVpZ2hib3VyLmV4cHJzW3RlYy5odmdzJEhWRywgXSkpKQpwYWlycyh0ZWMubmVpZ2hib3VyLnBjYSR4WywgYygxOjUpXSkKYGBgCgpgYGB7cn0Kc2V0LnNlZWQoNDIpCm5laWdoYm91cmhvb2QudW1hcCA8LSB1bWFwKHRlYy5uZWlnaGJvdXIucGNhJHhbLCBjKDE6MzApXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgbl9jb21wb25lbnRzPTIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG5fbmVpZ2hib3JzPTExLCBtZXRyaWM9J2V1Y2xpZGVhbicsCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGluaXQ9J3JhbmRvbScsIG1pbl9kaXN0PTAuMSkKYGBgCgpXZSBjYW4gb3ZlcmxheSB0aGUgREEgdGVzdGluZyBvbiB0aGVzZSBuZWlnaGJvdXJob29kcy4KCmBgYHtyfQp0ZWMubmVpZ2hib3IuZGYgPC0gdGVjLnJlc1ssIGMoImxvZ0ZDIiwgIk5laWdoYm91cmhvb2QiLCAiU3BhdGlhbEZEUiIpXQp0ZWMubmVpZ2hib3IuZGYgPC0gZG8uY2FsbChjYmluZC5kYXRhLmZyYW1lLCBsaXN0KHRlYy5uZWlnaGJvci5kZiwgYXMuZGF0YS5mcmFtZShuZWlnaGJvdXJob29kLnVtYXAkbGF5b3V0KSkpCmNvbG5hbWVzKHRlYy5uZWlnaGJvci5kZikgPC0gYygibG9nRkMiLCAiTmVpZ2hib3VyaG9vZCIsICJTcGF0aWFsRkRSIiwgIlVNQVAxIiwgIlVNQVAyIikKdGVjLm5laWdoYm9yLmRmJFNpZyA8LSBhcy5udW1lcmljKHRlYy5uZWlnaGJvci5kZiRTcGF0aWFsRkRSIDw9IDAuMDUpCgpnZ3Bsb3QodGVjLm5laWdoYm9yLmRmLCBhZXMoeD1VTUFQMSwgeT1VTUFQMikpICsKICBnZW9tX3BvaW50KGRhdGE9dGVjLm5laWdoYm9yLmRmW3RlYy5uZWlnaGJvci5kZiRTaWcgPT0gMCwgXSwKICAgICAgICAgICAgIGNvbG91cj0nZ3JleTgwJywgc2l6ZT0yKSArCiAgZ2VvbV9wb2ludChkYXRhPXRlYy5uZWlnaGJvci5kZlt0ZWMubmVpZ2hib3IuZGYkU2lnID09IDEsIF0sCiAgICAgICAgICAgICBhZXMoY29sb3VyPWxvZ0ZDKSwgc2l6ZT00KSArCiAgdGhlbWVfY2xlYW4oKSArCiAgc2NhbGVfY29sb3VyX2dyYWRpZW50Mihsb3c9ImJsdWUiLCBtaWQ9ImdyZXk4MCIsIGhpZ2g9InJlZCIpCmBgYAoKU29tZSBhcmUgdXAgYW5kIHNvbWUgYXJlIGRvd24uIFdoaWNoIFRFQyBzdWJ0eXBlcyBkbyB0aGV5IGxhcmdlbHkgY29ycmVzcG9uZCB3aXRoPyBUaGF0IGJpZyBzdHJlYWsgb2YgbG93ZXIgYWJ1bmRhbmNlIG5laWdoYm91cmhvb2RzIHNob3VsZCBiZSB0aGUgZGlmZmVyZW50aWF0aW9uIAp0cmFqZWN0b3J5IGZyb20gSW50ZXJ0eXBpY2FsIHRvIE1hdHVyZSBtVEVDLgoKYGBge3J9CnRlYy5uZWlnaGJvdXIubGlzdCA8LSBsaXN0KCkKZm9yKHggaW4gc2VxX2Fsb25nKDE6bGVuZ3RoKHRlYy52ZXJ0ZXgubGlzdCkpKXsKICB4LmRmIDwtIHRlYy51bWFwLm1lcmdlW3RlYy51bWFwLm1lcmdlJFZlcnRleCAlaW4lIHRlYy52ZXJ0ZXgubGlzdFtbeF1dLCBdCiAgeC5yZXAgPC0gbmFtZXModGFibGUoeC5kZiRTb3J0RGF5KSlbd2hpY2godGFibGUoeC5kZiRTb3J0RGF5KSA9PSBtYXgodGFibGUoeC5kZiRTb3J0RGF5KSkpXQogIGlmKGxlbmd0aCh4LnJlcCkgPiAxKXsKICAgIHgucmVwIDwtIHNhbXBsZShzaXplPTEsIHgucmVwKQogIH0KICB4LmJsb2NrIDwtIG5hbWVzKHRhYmxlKHguZGYkQ2x1c3RlcikpW3doaWNoKHRhYmxlKHguZGYkQ2x1c3RlcikgPT0gbWF4KHRhYmxlKHguZGYkQ2x1c3RlcikpKV0KICAgIGlmKGxlbmd0aCh4LmJsb2NrKSA+IDEpewogICAgeC5ibG9jayA8LSBzYW1wbGUoc2l6ZT0xLCB4LmJsb2NrKQogIH0KICB4LmNvbmRpdGlvbiA8LSBuYW1lcyh0YWJsZSh4LmRmJEFnZSkpW3doaWNoKHRhYmxlKHguZGYkQWdlKSA9PSBtYXgodGFibGUoeC5kZiRBZ2UpKSldCiAgICBpZihsZW5ndGgoeC5jb25kaXRpb24pID4gMSl7CiAgICB4LmNvbmRpdGlvbiA8LSBzYW1wbGUoc2l6ZT0xLCB4LmNvbmRpdGlvbikKICB9CiAgCiAgdGVjLm5laWdoYm91ci5saXN0W1t4XV0gPC0gZGF0YS5mcmFtZSgiUmVwbGljYXRlIj14LnJlcCwgIkNsdXN0ZXIiPXguYmxvY2ssICJDb25kaXRpb24iPXguY29uZGl0aW9uLCAiTmVpZ2hib3VyaG9vZCI9eCkKfQoKdGVjLm5laWdoYm91ci5tZXRhIDwtIGRvLmNhbGwocmJpbmQuZGF0YS5mcmFtZSwgdGVjLm5laWdoYm91ci5saXN0KQp0ZWMubmVpZ2hib3VyLm1lcmdlIDwtIG1lcmdlKHRlYy5uZWlnaGJvci5kZiwgdGVjLm5laWdoYm91ci5tZXRhLCBieT0nTmVpZ2hib3VyaG9vZCcpCgp0ZWMubmVpZ2hib3VyLm1lcmdlJERpZmYgPC0gc2lnbih0ZWMubmVpZ2hib3VyLm1lcmdlJGxvZ0ZDKQp0ZWMubmVpZ2hib3VyLm1lcmdlJERpZmZbdGVjLm5laWdoYm91ci5tZXJnZSRTaWcgPT0gMF0gPC0gMApgYGAKCgpgYGB7ciwgZmlnLndpZHRoPTkuNzUsIGZpZy5oZWlnaHQ9Ni4xNX0KZ2dwbG90KHRlYy5uZWlnaGJvdXIubWVyZ2UsIGFlcyh4PVVNQVAxLCB5PVVNQVAyKSkgKwogIGdlb21fcG9pbnQoZGF0YT10ZWMubmVpZ2hib3VyLm1lcmdlWywgYygiVU1BUDEiLCAiVU1BUDIiKV0sCiAgICAgICAgICAgICBjb2xvdXI9J2dyZXk4MCcsIHNpemU9MSkgKwogIGdlb21fcG9pbnQoZGF0YT10ZWMubmVpZ2hib3VyLm1lcmdlW3RlYy5uZWlnaGJvdXIubWVyZ2UkU2lnID09IDEsIF0sCiAgICAgICAgICAgICBhZXMoY29sb3VyPWxvZ0ZDKSwgc2l6ZT00KSArCiAgdGhlbWVfY2xlYW4oKSArCiAgc2NhbGVfY29sb3VyX2dyYWRpZW50Mihsb3c9ImJsdWUiLCBtaWQ9ImdyZXk4MCIsIGhpZ2g9InJlZCIpICsKICBmYWNldF93cmFwKH5DbHVzdGVyKQpgYGAKClRoaXMgZXh0ZW5kZWQgYW5hbHlzaXMgd2l0aCBrPTExIGFsc28gZGV0ZWN0cyB0aGUgaW5jcmVhc2UgaW4gdGhlIHNtYWxsIHNURUMgcG9wdWxhdGlvbi4gVGhlcmUgYWxzbyBhcHBlYXJzIHRvIGJlIG1vcmUgaGV0ZXJvZ2VuZWl0eSBpbiB0aGUgSW50ZXJ0eXBpY2FsIFRFQywgYW5kLCAKc29tZXdoYXQgaW5ndHJpZ3VpbmdseSwgY2hhbmdlcyBhbW9uZ3N0IHRoZSBtYXR1cmUgbVRFQyB3aGljaCB3ZXJlIG5vdCBkZXRlY3RlZCBvcmlnaW5hbGx5LgoKYGBge3J9CnRhYmxlKHRlYy5uZWlnaGJvdXIubWVyZ2UkQ2x1c3RlciwgdGVjLm5laWdoYm91ci5tZXJnZSREaWZmKQpgYGAKCkkgd291bGQgc2F5IHRoYXQgdGhlc2UgcmVzdWx0cyBtYWtlIGEgbG90IG9mIHNlbnNlLiBJJ2xsIGV4dGVuZCBpdCB0byBpbmNsdWRlIHRoZSBxdWFkcmF0aWMgdGVzdGluZyB3aGljaCBzaG91bGQgcGljayB1cCB0aGUgaW52ZXJzZS1wYXJhYm9saWMgcHJvZmlsZSBvZiAKdGhlIFBvc3QtQWlyZSBtVEVDIHBvcHVsYXRpb24uCgpgYGB7cn0KcXVhZC50ZWMucmVzIDwtIGFzLmRhdGEuZnJhbWUodG9wVGFncyhnbG1RTEZUZXN0KHRlYy5maXQsIGNvZWY9MyksIHNvcnQuYnk9J25vbmUnLCBuPUluZikpCnF1YWQudGVjLnJlcyRTaWcgPC0gYXMuZmFjdG9yKGFzLm51bWVyaWMocXVhZC50ZWMucmVzJEZEUiA8PSAwLjA1KSkKcXVhZC50ZWMucmVzJE5laWdoYm91cmhvb2QgPC0gYXMubnVtZXJpYyhyb3duYW1lcyhxdWFkLnRlYy5yZXMpKQoKIyBjb250cm9sIHRoZSBzcGF0aWFsIEZEUgpxdmFscyA8LSBxdWFkLnRlYy5yZXMkUFZhbHVlCmlzLnNpZyA8LSBxdmFscyA8PSAwLjA1CnN1bW1hcnkoaXMuc2lnKQpgYGAKClRoZXJlIGFyZSAyNyBEQSBuZWlnaGJvdXJob29kcyBmcm9tIHRoZSBxdWFkcmF0aWMgbW9kZWwgLSBJIGV4cGVjdCB0aGVzZSBzaG91bGQgcmVmbGVjdCB0aGUgUG9zdC1BaXJlIG1URUMuIAoKYGBge3J9CnF1YWQudGVjLnNwYXRpYWxmZHIgPC0gZ3JhcGhfc3BhdGlhbEZEUihuZWlnaGJvcmhvb2RzPXRlYy52ZXJ0ZXgubGlzdCwgZ3JhcGg9dGVjLmtubiwgY29ubmVjdGl2aXR5PSJkaXN0YW5jZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGNhPWFzLm1hdHJpeCh0ZWMuc3ViLm1ldGFbLCBwYXN0ZTAoIlBDIiwgMTozMCldKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwdmFsdWVzPXF1YWQudGVjLnJlc1tvcmRlcihxdWFkLnRlYy5yZXMkTmVpZ2hib3VyaG9vZCksIF0kUFZhbHVlKQpxdWFkLnRlYy5yZXMkU3BhdGlhbEZEUltvcmRlcihxdWFkLnRlYy5yZXMkTmVpZ2hib3VyaG9vZCldIDwtIHF1YWQudGVjLnNwYXRpYWxmZHIKcXZhbHMgPC0gcXVhZC50ZWMuc3BhdGlhbGZkcgppcy5zaWcgPC0gcXZhbHMgPD0gMC4wNQpzdW1tYXJ5KGlzLnNpZykKYGBgCgpEYW5nLCBjbGVhcmx5IHN0aWxsIHRoZSBzbWFsbCBncm91cCBvZiBQb3N0LUFpcmUgbVRFQyBtZWFucyB0aGlzIGlzbid0IHN1ZmZpY2llbnRseSBzZW5zaXRpdmUgYWZ0ZXIgbXVsdGlwbGUtdGVzdGluZyBjb3JyZWN0aW9uLgoKYGBge3IsIGZpZy53aWR0aD05Ljc1LCBmaWcuaGVpZ2h0PTYuMTV9CmdncGxvdCh0ZWMubmVpZ2hib3VyLm1lcmdlLCBhZXMoeD1VTUFQMSwgeT1VTUFQMikpICsKICBnZW9tX3BvaW50KGRhdGE9dGVjLm5laWdoYm91ci5tZXJnZVssIGMoIlVNQVAxIiwgIlVNQVAyIildLAogICAgICAgICAgICAgY29sb3VyPSdncmV5ODAnLCBzaXplPTEpICsKICBnZW9tX3BvaW50KGRhdGE9dGVjLm5laWdoYm91ci5tZXJnZSwKICAgICAgICAgICAgIGFlcyhjb2xvdXI9Q2x1c3RlciksIHNpemU9MykgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzPWludGVyLmNvbHMpICsKICBmYWNldF93cmFwKH5DbHVzdGVyKQpgYGAKCkhtbSwgdGhlIFBvc3QtQWlyZSBtVEVDIGRvbid0IGZvcm0gYSBzaW5nbGUgbmVpZ2hib3VyaG9vZCBvbiB0aGVpciBvd24gYnV0IHJhdGhlciAzIHNlcGFyYXRlIG9uZXMuIEkgd291bGQgc2F5IHRoaXMgcG9zc2libHkgdG9vIGdyYW51bGFyLgoKIyMgQ29tcG9zaXRpb25hbCBlZmZlY3RzCgpGaXJzdGx5LCBhcmUgY29tcG9zaXRpb25hbCBlZmZlY3RzIGEgcHJvYmxlbSBoZXJlLCBhbmQgc2Vjb25kbHksIGRvZXMgdGhlIHJlZmluZWQgc2FtcGxpbmcgc2NoZW1lIGhhbmRsZSB0aGlzPyBJJ2xsIHNldCB1cCBhIG5ldyBzaW11bGF0aW9uIHRoYXQgaGFzIDIgCmNsdXN0ZXJzLCBvbmx5IG9uZSBvZiB3aGljaCBjb250YWlucyBkaWZmZXJlbnRpYWxseSBhYnVuZGFudCBuZWlnaGJvdXJob29kcy4KCmBgYHtyLCBlY2hvPVRSVUUsIHdhcm5pbmc9RkFMU0V9CnNldC5zZWVkKDQyKQpyLm4gPC0gMTAwMApuLmRpbSA8LSA1MApibG9jazEuY2VsbHMgPC0gMjUwCiMgc2VsZWN0IGEgc2V0IG9mIGVpZ2VuIHZhbHVlcyBmb3IgdGhlIGNvdmFyaWFuY2UgbWF0cml4IG9mIGVhY2ggYmxvY2ssIHNheSA1MCBlaWdlbnZhbHVlcz8KYmxvY2sxLmVpZ2VucyA8LSBzYXBwbHkoMTpuLmRpbSwgRlVOPWZ1bmN0aW9uKFgpIHJleHAobj0xLCByYXRlPWFicyhydW5pZihuPTEsIG1pbj0wLCBtYXg9NTApKSkpCmJsb2NrMS5laWdlbnMgPC0gYmxvY2sxLmVpZ2Vuc1tvcmRlcihibG9jazEuZWlnZW5zKV0KYmxvY2sxLnAgPC0gcXIuUShxcihtYXRyaXgocm5vcm0oYmxvY2sxLmNlbGxzXjIsIG1lYW49NCwgc2Q9MC4wMSksIGJsb2NrMS5jZWxscykpKQpibG9jazEuc2lnbWEgPC0gY3Jvc3Nwcm9kKGJsb2NrMS5wLCBibG9jazEucCpibG9jazEuZWlnZW5zKQpibG9jazEuZ2V4IDwtIGFicyhybXZub3JtKG49ci5uLCBtZWFuPXJub3JtKG49YmxvY2sxLmNlbGxzLCBtZWFuPTIsIHNkPTAuMDEpLCBzaWdtYT1ibG9jazEuc2lnbWEpKQoKCmJsb2NrMy5jZWxscyA8LSAyNTAKIyBzZWxlY3QgYSBzZXQgb2YgZWlnZW4gdmFsdWVzIGZvciB0aGUgY292YXJpYW5jZSBtYXRyaXggb2YgZWFjaCBibG9jaywgc2F5IDUwIGVpZ2VudmFsdWVzPwpibG9jazMuZWlnZW5zIDwtIHNhcHBseSgxOm4uZGltLCBGVU49ZnVuY3Rpb24oWCkgcmV4cChuPTEsIHJhdGU9YWJzKHJ1bmlmKG49MSwgbWluPTAsIG1heD01MCkpKSkKYmxvY2szLmVpZ2VucyA8LSBibG9jazMuZWlnZW5zW29yZGVyKGJsb2NrMy5laWdlbnMpXQpibG9jazMucCA8LSBxci5RKHFyKG1hdHJpeChybm9ybShibG9jazMuY2VsbHNeMiwgbWVhbj00LCBzZD0wLjAxKSwgYmxvY2szLmNlbGxzKSkpCmJsb2NrMy5zaWdtYSA8LSBjcm9zc3Byb2QoYmxvY2szLnAsIGJsb2NrMy5wKmJsb2NrMy5laWdlbnMpCmJsb2NrMy5nZXggPC0gYWJzKHJtdm5vcm0obj1yLm4sIG1lYW49cm5vcm0obj1ibG9jazMuY2VsbHMsIG1lYW49NSwgc2Q9MC4wMSksIHNpZ21hPWJsb2NrMy5zaWdtYSkpCgpzaW0yLmdleCA8LSBkby5jYWxsKGNiaW5kLCBsaXN0KCJiMSI9YmxvY2sxLmdleCwgImIzIj1ibG9jazMuZ2V4KSkKYGBgCgoKYGBge3J9CnNpbTIucGNhIDwtIHByY29tcF9pcmxiYSh0KHNpbTIuZ2V4KSwgbj01MCwgc2NhbGUuPVRSVUUsIGNlbnRlcj1UUlVFKQpwYWlycyhzaW0yLnBjYSR4WywgYygxOjUpXSkKYGBgCgpJJ2xsIHVzZSB0aGUgcmVkdWNlZCBkaW1lbnNpb25zIGhlcmUgdG8gY29tcHV0ZSBhIEtOTi1ncmFwaCBhbmQgdmlzdWFsaXNlIGl0IHVzaW5nIGEgRnJ1Y3Rlcm1hbi1SZWluZ29sZCBsYXlvdXQuCgpgYGB7cn0Kc2V0LnNlZWQoNDIpCnNpbTIua25uIDwtIGJ1aWxkS05OR3JhcGgoeD1zaW0yLnBjYSR4WywgYygxOjMwKV0sIGs9MjEsIGQ9TkEsIHRyYW5zcG9zZWQ9VFJVRSkKc2ltMi5mci5sYXlvdXQgPC0gbGF5b3V0X3dpdGhfZnIoc2ltMi5rbm4pCnBsb3Qoc2ltMi5rbm4sIGxheW91dD1zaW0yLmZyLmxheW91dCwgdmVydGV4LmZyYW1lLmNvbG9yPSdza3libHVlJywgdmVydGV4LmNvbG9yPSdza3libHVlJywgdmVydGV4LmxhYmVsLmNvbG9yPSdibGFjaycsIAogICAgIHZlcnRleC5sYWJlbC5mYW1pbHk9J0hlbHZldGljYScsIGVkZ2UuY29sb3I9J2dyZXk2MCcsIHZlcnRleC5sYWJlbC5jZXg9MC45LAogICAgIHZlcnRleC5sYWJlbC5kaXN0PTEsIGVkZ2UuYXJyb3cuc2l6ZT0wLjIpCmBgYAoKQWxzbyBhIFVNQVAgbGF5b3V0LgoKYGBge3J9CnNldC5zZWVkKDQyKQpzdGVtLnRhLnVtYXAgPC0gdW1hcChzaW0yLnBjYSR4WywgYygxOjMwKV0sCiAgICAgICAgICAgICAgICAgICAgIG5fY29tcG9uZW50cz0yLAogICAgICAgICAgICAgICAgICAgICBuX25laWdoYm9ycz0yMSwgbWV0cmljPSdldWNsaWRlYW4nLAogICAgICAgICAgICAgICAgICAgICBpbml0PSdyYW5kb20nLCBtaW5fZGlzdD0wLjEpCnBsb3Qoc3RlbS50YS51bWFwJGxheW91dCwgY29sPWMocmVwKCJyZWQiLCBibG9jazEuY2VsbHMpLCByZXAoIm9yYW5nZSIsIGJsb2NrMy5jZWxscykpLAogICAgIHhsYWI9IlVNQVAgMSIsIHlsYWI9IlVNQVAgMiIpCmBgYAoKV2l0aGluIGVhY2ggb2YgdGhlc2UgY2xvdWRzIG9mIHBvaW50cyBJIHdpbGwgcmFuZG9tbHkgbGFiZWwgMTo5IGluIGJsb2NrIDEgYW5kIDE6MSBpbiBibG9jayAyLgoKYGBge3J9CnNldC5zZWVkKDQyKQpibG9jazEuY29uZCA8LSByZXAoIkEiLCBibG9jazEuY2VsbHMpCmJsb2NrMS5hIDwtIHNhbXBsZSgxOmJsb2NrMS5jZWxscywgc2l6ZT1mbG9vcihibG9jazEuY2VsbHMqMC4xKSkKYmxvY2sxLmIgPC0gc2V0ZGlmZigxOmJsb2NrMS5jZWxscywgYmxvY2sxLmEpCmJsb2NrMS5jb25kW2Jsb2NrMS5iXSA8LSAiQiIKCmJsb2NrMy5jb25kIDwtIHJlcCgiQSIsIGJsb2NrMy5jZWxscykKYmxvY2szLmEgPC0gc2FtcGxlKDE6YmxvY2szLmNlbGxzLCBzaXplPWZsb29yKGJsb2NrMy5jZWxscyowLjUpKQpibG9jazMuYiA8LSBzZXRkaWZmKDE6YmxvY2szLmNlbGxzLCBibG9jazMuYSkKYmxvY2szLmNvbmRbYmxvY2szLmJdIDwtICJCIgoKbWV0YS5kZiA8LSBkYXRhLmZyYW1lKCJCbG9jayI9YyhyZXAoIkIxIiwgYmxvY2sxLmNlbGxzKSwgcmVwKCJCMyIsIGJsb2NrMy5jZWxscykpLAogICAgICAgICAgICAgICAgICAgICAgIkNvbmRpdGlvbiI9YyhibG9jazEuY29uZCwgYmxvY2szLmNvbmQpLAogICAgICAgICAgICAgICAgICAgICAgIlJlcGxpY2F0ZSI9YyhyZXAoIlIxIiwgZmxvb3IoYmxvY2sxLmNlbGxzKjAuMzMpKSwgcmVwKCJSMiIsIGZsb29yKGJsb2NrMS5jZWxscyowLjMzKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXAoIlIzIiwgYmxvY2sxLmNlbGxzLSgyKmZsb29yKGJsb2NrMS5jZWxscyowLjMzKSkpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXAoIlIxIiwgZmxvb3IoYmxvY2szLmNlbGxzKjAuMzMpKSwgcmVwKCJSMiIsIGZsb29yKGJsb2NrMy5jZWxscyowLjMzKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXAoIlIzIiwgYmxvY2szLmNlbGxzLSgyKmZsb29yKGJsb2NrMy5jZWxscyowLjMzKSkpKSkKbWV0YS5kZiA8LSBjYmluZChtZXRhLmRmLCBzdGVtLnRhLnVtYXAkbGF5b3V0KQpjb2xuYW1lcyhtZXRhLmRmKSA8LSBjKCJCbG9jayIsICJDb25kaXRpb24iLCAiUmVwbGljYXRlIiwgIlVNQVAxIiwgIlVNQVAyIikKIyBkZWZpbmUgYSAic2FtcGxlIiBhcyB0ZWggY29tYmluYXRpb24gb2YgY29uZGl0aW9uIGFuZCByZXBsaWNhdGUKbWV0YS5kZiRTYW1wbGUgPC0gcGFzdGUobWV0YS5kZiRDb25kaXRpb24sIG1ldGEuZGYkUmVwbGljYXRlLCBzZXA9Il8iKQptZXRhLmRmJFZlcnRleCA8LSBjKDE6bnJvdyhtZXRhLmRmKSkKYGBgCgoKYGBge3J9CmdncGxvdChtZXRhLmRmLCBhZXMoeD1VTUFQMSwgeT1VTUFQMikpICsKICBnZW9tX3BvaW50KGFlcyhjb2xvdXI9QmxvY2ssIHNoYXBlPVJlcGxpY2F0ZSkpICsKICB0aGVtZV9jbGVhbigpICsKICBzY2FsZV9jb2xvdXJfbnBnKCkgKwogIGZhY2V0X3dyYXAofkNvbmRpdGlvbikgKwogIGd1aWRlcyhjb2xvdXI9Z3VpZGVfbGVnZW5kKG92ZXJyaWRlLmFlcz1saXN0KHNpemU9MykpLAogICAgICAgICBzaGFwZT1ndWlkZV9sZWdlbmQob3ZlcnJpZGUuYWVzPWxpc3Qoc2l6ZT0zKSkpCmBgYAoKCgpgYGB7ciwgZWNobz1GQUxTRSwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0Kc2V0LnNlZWQoNDIpCnJlZmluZV92ZXJ0ZXggPC0gZnVuY3Rpb24odmVydGV4Lmtubiwgdi5peCwgWF9wY2EpewogICMgdmVydGV4LmtubjogS05OIGdyYXBoIGZvciByYW5kb21seSBzYW1wbGVkIHBvaW50cyAob3V0cHV0IG9mIEJpb2NOZWlnaGJvcnM6OmZpbmRLTk4pCiAgIyB2Lml4OiBpbmRleCBvZiB2ZXJ0ZXggdG8gcmVmaW5lIGluIHZlcnRleC5rbm4KICAKICAjIyBDYWxjdWxhdGUgbWVkaWFuIHByb2ZpbGUgb2YgS05OcyBvZiB2ZXJ0ZXgKICB2Lm1lZCA8LSBhcHBseShYX3BjYVt2ZXJ0ZXgua25uJGluZGV4W3YuaXgsXSxdLCAyLCBtZWRpYW4pCiAgIyMgRmluZCB0aGUgY2xvc2VzdCBwb2ludCB0byB0aGUgbWVkaWFuIGFuZCBzYW1wbGUKICByZWZpbmVkLnZlcnRleCA8LSBCaW9jTmVpZ2hib3JzOjpmaW5kS05OKHJiaW5kKHYubWVkLCBYX3BjYSksIHN1YnNldD0xLCBrPTEpW1siaW5kZXgiXV1bMV0gLSAxICMjIC0xIHRvIHJlbW92ZSB0aGUgbWVkaWFuCiAgcmV0dXJuKHJlZmluZWQudmVydGV4KQp9CgpncmFwaCA8LSBzaW0yLmtubgpzYW1wbGUudmVydGljZXMgPC0gMC4xClhfcGNhIDwtIHNpbTIucGNhJHhbLCBjKDE6MzApXQoKcmFuZG9tLnZlcnRpY2VzIDwtIHNhbXBsZShWKGdyYXBoKSwgc2l6ZT1mbG9vcihzYW1wbGUudmVydGljZXMqbGVuZ3RoKFYoZ3JhcGgpKSkpCnZlcnRleC5rbm4gPC0gQmlvY05laWdoYm9yczo6ZmluZEtOTihYPVhfcGNhLCBrPTIxLCBzdWJzZXQ9YXMudmVjdG9yKHJhbmRvbS52ZXJ0aWNlcykpCnJlZmluZWQudmVydGljZXMgPC0gVihncmFwaClbc2FwcGx5KDE6bnJvdyh2ZXJ0ZXgua25uJGluZGV4KSwgZnVuY3Rpb24oaSkgcmVmaW5lX3ZlcnRleCh2ZXJ0ZXgua25uLCBpLCBYX3BjYSkpXQoKdmVydGV4Lmxpc3QgPC0gc2FwcGx5KDE6bGVuZ3RoKHJhbmRvbS52ZXJ0aWNlcyksIEZVTj1mdW5jdGlvbihYKSBuZWlnaGJvcnMoZ3JhcGgsIHY9cmFuZG9tLnZlcnRpY2VzW1hdKSkKdmVydGV4Lmxpc3QucmVmaW5lZCA8LSBzYXBwbHkoMTpsZW5ndGgocmVmaW5lZC52ZXJ0aWNlcyksIEZVTj1mdW5jdGlvbihYKSBuZWlnaGJvcnMoZ3JhcGgsIHY9cmVmaW5lZC52ZXJ0aWNlc1tYXSkpCgpwbG90KGhpc3QodW5saXN0KGxhcHBseSh2ZXJ0ZXgubGlzdC5yZWZpbmVkLCBsZW5ndGgpKSwgMTAwLCBwbG90PUZBTFNFKSwgY29sPSdncmVlbicsIG1haW49Ikhpc3RvZ3JhbSBvZiBuZWlnaGJvcnMiLCB4bGFiPSJOZWlnaGJvdXJob29kIHNpemUiKQpwbG90KGhpc3QodW5saXN0KGxhcHBseSh2ZXJ0ZXgubGlzdCwgbGVuZ3RoKSksIDEwMCwgcGxvdD1GQUxTRSksICBjb2w9J2JsdWUnLCBhZGQ9VFJVRSkKYGBgCgpUaGUgcmVmaW5lZCBzYW1wbGluZyBzY2hlbWUgbGVhZHMgdG8gbGFyZ2UgbmVpZ2hib3VyaG9vZHMgb3ZlcmFsbCAtIHdlIHRoaW5rIHRoaXMgbWlnaHQgaW5jcmVhc2UgcG93ZXIgYW5kIHNlbnNpdGl2aXR5IGFzIHRoZSBjb3VudHMgaW4gZWFjaCB3aWxsIGFsc28gYmUgCmxhcmdlciBhbmQgdGhlcmVmb3JlIG1vcmUgc3RhYmxlLgoKIyMjIFRlc3QgdXNpbmcgcmFuZG9tIHNhbXBsaW5nCgpgYGB7cn0Kc2ltMi5jb3VudHMgPC0gcXVhbnRfbmVpZ2hib3VyaG9vZChncmFwaD1zaW0yLmtubiwgbWV0YT1tZXRhLmRmLCBzYW1wbGUuY29sdW1uPSdTYW1wbGUnLCBzYW1wbGUudmVydGljZXM9bi5ob29kKQpzYW1wbGUubWV0YSA8LSBkYXRhLmZyYW1lKCJDb25kaXRpb24iPWMocmVwKCJBIiwgMyksIHJlcCgiQiIsIDMpKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAiUmVwbGljYXRlIj1yZXAoYygiUjEiLCAiUjIiLCAiUjMiKSwgMikpCnNhbXBsZS5tZXRhJFNhbXBsZSA8LSBwYXN0ZShzYW1wbGUubWV0YSRDb25kaXRpb24sIHNhbXBsZS5tZXRhJFJlcGxpY2F0ZSwgc2VwPSJfIikKcm93bmFtZXMoc2FtcGxlLm1ldGEpIDwtIHNhbXBsZS5tZXRhJFNhbXBsZQojIHNpbTIubW9kZWwgPC0gbW9kZWwubWF0cml4KH4gMCArIENvbmRpdGlvbiwgZGF0YT1zYW1wbGUubWV0YSkKc2ltMi5tb2RlbCA8LSBtb2RlbC5tYXRyaXgofiBDb25kaXRpb24sIGRhdGE9c2FtcGxlLm1ldGEpCmhlYWQoc2ltMi5tb2RlbCkKYGBgCgpJIGhhdmUgYSBtb2RlbCBtYXRyaXggYW5kIGNvdW50cyBtYXRyaXggLSBsZXQncyB0ZXN0IGVkZ2VSIG9uIHRoZXNlLgoKYGBge3J9CmNvdW50Lm1lYW5zIDwtIHJvd01lYW5zKHNpbTIuY291bnRzWywgcm93bmFtZXMoc2ltMi5tb2RlbCldKQpjb3VudC52YXJzIDwtIGFwcGx5KHNpbTIuY291bnRzWywgcm93bmFtZXMoc2ltMi5tb2RlbCldLCAxLCB2YXIpCgpwbG90KGNvdW50Lm1lYW5zLCBjb3VudC52YXJzKQpgYGAKClRoZSBkYXRhIGFyZSBvdmVyZGlzcGVyc2VkLiBUaGUgbW9kZWwgbm9ybWFsaXNhdGlvbiBpcyBjYXVzaW5nIHNvbWUgY29uc3Rlcm5hdGlvbi4gVGhlc2UgYWxsIHJlbHkgb24gbm9ybWFsaXNpbmcgdGhlIG5laWdoYm91cmhvb2QgY291bnRzIGJ5IHNvbWUgZmFjdG9yLiAKV2hhdCBpZiB0aGUgbm9ybWFsaXNhdGlvbiB1c2VzIHRoZSB0b3RhbCBudW1iZXIgb2YgY2VsbHMgaW4gdGhlIGV4cGVyaW1lbnQgZm9yIGVhY2ggc2FtcGxlLCByYXRoZXIgdGhhbiB0aGUgY291bnRzIGluIG5laWdoYm91cmhvb2RzLCB3aGljaCB3aWxsIGFsd2F5cyBiZSAKaGlnaGVyIGJlY2F1c2UgY2VsbHMgYXJlIGNvdW50ZWQgbXVsdGlwbGUgdGltZXMuCgpgYGB7cn0Kc2ltMi5kZ2UgPC0gREdFTGlzdChzaW0yLmNvdW50c1ssIHJvd25hbWVzKHNpbTIubW9kZWwpXSwgbGliLnNpemU9bG9nKGNvbFN1bXMoc2ltMi5jb3VudHNbLCByb3duYW1lcyhzaW0yLm1vZGVsKV0pKSkKCnNpbTIuZGdlIDwtIGVzdGltYXRlRGlzcChzaW0yLmRnZSwgc2ltMi5tb2RlbCkKc2ltMi5maXQgPC0gZ2xtUUxGaXQoc2ltMi5kZ2UsIHNpbTIubW9kZWwsIHJvYnVzdD1UUlVFKQoKc2ltMi5yZXMgPC0gYXMuZGF0YS5mcmFtZSh0b3BUYWdzKGdsbVFMRlRlc3Qoc2ltMi5maXQsIGNvZWY9MiksIHNvcnQuYnk9J25vbmUnLCBuPUluZikpCnNpbTIucmVzJE5laWdoYm91cmhvb2QgPC0gYXMubnVtZXJpYyhyb3duYW1lcyhzaW0yLnJlcykpCgpzaW0yLnNwYXRpYWxmZHIgPC0gZ3JhcGhfc3BhdGlhbEZEUihuZWlnaGJvcmhvb2RzPXZlcnRleC5saXN0LCBncmFwaD1zaW0yLmtubiwgY29ubmVjdGl2aXR5PSJkaXN0YW5jZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHB2YWx1ZXM9c2ltMi5yZXNbb3JkZXIoc2ltMi5yZXMkTmVpZ2hib3VyaG9vZCksIF0kUFZhbHVlLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGNhPXNpbTIucGNhJHhbLCBjKDE6MzApXSkKCnNpbTIucmVzJFNwYXRpYWxGRFJbb3JkZXIoc2ltMi5yZXMkTmVpZ2hib3VyaG9vZCldIDwtIHNpbTIuc3BhdGlhbGZkcgpxdmFscyA8LSBzaW0yLnNwYXRpYWxmZHIKaXMuc2lnIDwtIHF2YWxzIDw9IDAuMDEKc3VtbWFyeShpcy5zaWcpCmBgYAoKVGhpcyBpcyBhdCBhIDElIEZEUi4KCmBgYHtyfQpzaW0yLm5laWdoYm91ci5leHBycyA8LSBuZWlnaGJvcmhvb2RfZXhwcmVzc2lvbih2ZXJ0ZXgubGlzdCwgc2ltMi5nZXgpCmBgYAoKRW1iZWQgdGhlc2UgaHlwZXJzcGhlcmVzIHdpdGggYSBQQ0EgYW5kIFVNQVAuCgpgYGB7cn0Kc2ltMi5uZWlnaGJvdXIucGNhIDwtIHByY29tcCgodChzaW0yLm5laWdoYm91ci5leHBycykpKQpgYGAKCgpgYGB7cn0Kc2V0LnNlZWQoNDIpCm5laWdoYm91cmhvb2QudW1hcCA8LSB1bWFwKHNpbTIubmVpZ2hib3VyLnBjYSR4WywgYygxOjMwKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG5fY29tcG9uZW50cz0yLAogICAgICAgICAgICAgICAgICAgICAgICAgICBuX25laWdoYm9ycz0yMSwgbWV0cmljPSdldWNsaWRlYW4nLAogICAgICAgICAgICAgICAgICAgICAgICAgICBpbml0PSdyYW5kb20nLCBtaW5fZGlzdD0wLjEpCnBsb3QobmVpZ2hib3VyaG9vZC51bWFwJGxheW91dCwKICAgICB4bGFiPSJVTUFQIDEiLCB5bGFiPSJVTUFQIDIiKQpgYGAKCldlIGNhbiBvdmVybGF5IHRoZSBEQSB0ZXN0aW5nIG9uIHRoZXNlIG5laWdoYm91cmhvb2RzLgoKYGBge3J9Cm5laWdoYm9yLmRmIDwtIHNpbTIucmVzWywgYygibG9nRkMiLCAiTmVpZ2hib3VyaG9vZCIsICJTcGF0aWFsRkRSIildCm5laWdoYm9yLmRmIDwtIGRvLmNhbGwoY2JpbmQuZGF0YS5mcmFtZSwgbGlzdChuZWlnaGJvci5kZiwgYXMuZGF0YS5mcmFtZShuZWlnaGJvdXJob29kLnVtYXAkbGF5b3V0KSkpCmNvbG5hbWVzKG5laWdoYm9yLmRmKSA8LSBjKCJsb2dGQyIsICJOZWlnaGJvdXJob29kIiwgIlNwYXRpYWxGRFIiLCAiVU1BUDEiLCAiVU1BUDIiKQpuZWlnaGJvci5kZiRTaWcgPC0gYXMubnVtZXJpYyhuZWlnaGJvci5kZiRTcGF0aWFsRkRSIDw9IDAuMDEpCgpnZ3Bsb3QobmVpZ2hib3IuZGYsIGFlcyh4PVVNQVAxLCB5PVVNQVAyKSkgKwogIGdlb21fcG9pbnQoZGF0YT1uZWlnaGJvci5kZltuZWlnaGJvci5kZiRTaWcgPT0gMCwgXSwKICAgICAgICAgICAgIGNvbG91cj0nZ3JleTgwJywgc2l6ZT0yKSArCiAgZ2VvbV9wb2ludChkYXRhPW5laWdoYm9yLmRmW25laWdoYm9yLmRmJFNpZyA9PSAxLCBdLAogICAgICAgICAgICAgYWVzKGNvbG91cj1sb2dGQyksIHNpemU9NCkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9ncmFkaWVudDIobG93PSJibHVlIiwgbWlkPSJncmV5ODAiLCBoaWdoPSJyZWQiKQpgYGAKCklzIHRoYXQgYSBzdWJ0bGUgY29tcG9zaXRpb25hbCBlZmZlY3QgaW4gdGhlIDEgbmVpZ2hib3VyaG9vZCB0aGF0IGlzIGRlcGxldGVkPyBUaGF0IGNsdXN0ZXIgc2hvdWxkIG5vdCBjb250YWluIF9hbnlfIERBIG5laWdoYm91cmhvb2RzLgoKYGBge3J9Cm5laWdoYm91ci5saXN0IDwtIGxpc3QoKQpmb3IoeCBpbiBzZXFfYWxvbmcoMTpsZW5ndGgodmVydGV4Lmxpc3QpKSl7CiAgeC5kZiA8LSBtZXRhLmRmW21ldGEuZGYkVmVydGV4ICVpbiUgdmVydGV4Lmxpc3RbW3hdXSwgXQogIHgucmVwIDwtIG5hbWVzKHRhYmxlKHguZGYkUmVwbGljYXRlKSlbd2hpY2godGFibGUoeC5kZiRSZXBsaWNhdGUpID09IG1heCh0YWJsZSh4LmRmJFJlcGxpY2F0ZSkpKV0KICBpZihsZW5ndGgoeC5yZXApID4gMSl7CiAgICB4LnJlcCA8LSBzYW1wbGUoc2l6ZT0xLCB4LnJlcCkKICB9CiAgeC5ibG9jayA8LSBuYW1lcyh0YWJsZSh4LmRmJEJsb2NrKSlbd2hpY2godGFibGUoeC5kZiRCbG9jaykgPT0gbWF4KHRhYmxlKHguZGYkQmxvY2spKSldCiAgICBpZihsZW5ndGgoeC5ibG9jaykgPiAxKXsKICAgIHguYmxvY2sgPC0gc2FtcGxlKHNpemU9MSwgeC5ibG9jaykKICB9CiAgeC5jb25kaXRpb24gPC0gbmFtZXModGFibGUoeC5kZiRDb25kaXRpb24pKVt3aGljaCh0YWJsZSh4LmRmJENvbmRpdGlvbikgPT0gbWF4KHRhYmxlKHguZGYkQ29uZGl0aW9uKSkpXQogICAgaWYobGVuZ3RoKHguY29uZGl0aW9uKSA+IDEpewogICAgeC5jb25kaXRpb24gPC0gc2FtcGxlKHNpemU9MSwgeC5jb25kaXRpb24pCiAgfQogIAogIG5laWdoYm91ci5saXN0W1t4XV0gPC0gZGF0YS5mcmFtZSgiUmVwbGljYXRlIj14LnJlcCwgIkJsb2NrIj14LmJsb2NrLCAiQ29uZGl0aW9uIj14LmNvbmRpdGlvbiwgIk5laWdoYm91cmhvb2QiPXgpCn0KCm5laWdoYm91ci5tZXRhIDwtIGRvLmNhbGwocmJpbmQuZGF0YS5mcmFtZSwgbmVpZ2hib3VyLmxpc3QpCm5laWdoYm91ci5tZXJnZSA8LSBtZXJnZShuZWlnaGJvci5kZiwgbmVpZ2hib3VyLm1ldGEsIGJ5PSdOZWlnaGJvdXJob29kJykKbmVpZ2hib3VyLm1lcmdlJEJsb2NrIDwtIG9yZGVyZWQobmVpZ2hib3VyLm1lcmdlJEJsb2NrLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHM9YygiQjEiLCAiQjMiKSkKbmVpZ2hib3VyLm1lcmdlJERpZmYgPC0gc2lnbihuZWlnaGJvdXIubWVyZ2UkbG9nRkMpCm5laWdoYm91ci5tZXJnZSREaWZmW25laWdoYm91ci5tZXJnZSRTaWcgPT0gMF0gPC0gMApgYGAKCgpgYGB7ciwgZmlnLndpZHRoPTkuNzUsIGZpZy5oZWlnaHQ9NC4xNX0KZ2dwbG90KG5laWdoYm91ci5tZXJnZSwgYWVzKHg9VU1BUDEsIHk9VU1BUDIpKSArCiAgZ2VvbV9wb2ludChkYXRhPW5laWdoYm91ci5tZXJnZVssIGMoIlVNQVAxIiwgIlVNQVAyIildLAogICAgICAgICAgICAgY29sb3VyPSdncmV5ODAnLCBzaXplPTEpICsKICBnZW9tX3BvaW50KGRhdGE9bmVpZ2hib3VyLm1lcmdlW25laWdoYm91ci5tZXJnZSRTaWcgPT0gMSwgXSwKICAgICAgICAgICAgIGFlcyhjb2xvdXI9bG9nRkMpLCBzaXplPTQpICsKICB0aGVtZV9jbGVhbigpICsKICBzY2FsZV9jb2xvdXJfZ3JhZGllbnQyKGxvdz0iYmx1ZSIsIG1pZD0iZ3JleTgwIiwgaGlnaD0icmVkIikgKwogIGZhY2V0X3dyYXAofkJsb2NrKQpgYGAKClRoaXMgZG9lc24ndCBtYWtlIHNlbnNlIC0gQmxvY2sgMyBzaG91bGRuJ3QgaGF2ZSBhbnkgREEgbmVpZ2hib3VyaG9vZHMuIElzIHRoaXMgYSBjb21wb3NpdGlvbmFsIGVmZmVjdCB3ZSdyZSBzZWVpbmcgaGVyZT8gSXQncyBzdHJhbmdlIAp0aGF0IGEgcmFuZG9tIGZsdWN0dWF0aW9uIHdvdWxkIGNhdXNlIHRoaXMgLSBpdCBtdXN0IGJlIGluY3JlZGlibHkgc2Vuc2l0aXZlLiBUaGlzIGlzIGFsc28gc2FtcGxlLXNpemUgZGVwZW5kZW50LCBzbWFsbGVyIHRvdGFsIHNhbXBsZSBzaXplcyBhcmUgbGVzcyAKc3VzY2VwdGlibGUgZm9yIHNvbWUgcmVhc29uLgoKYGBge3J9CnRhYmxlKG5laWdoYm91ci5tZXJnZSRCbG9jaywgbmVpZ2hib3VyLm1lcmdlJERpZmYpCmBgYAoKVGhhdCBzaW5nbGUgZGVwbGV0ZWQgbmVpZ2hib3VyaG9vZCBpbiBCMyBpcywgSSB0aGluaywgYSBjb21wb3NpdGlvbmFsIGVmZmVjdC4gRG9lcyB0aGUgcmVmaW5lZCBzYW1wbGluZyBkZWFsIHdpdGggdGhpcyBpbiBzb21lIHdheSwgZWl0aGVyIGJ5IGhhdmluZyAKbmVpZ2hib3VyaG9vZHMgd2l0aCBsYXJnZXIsIGFuZCB0aHVzIG1vcmUgc3RhYmxlIGNvdW50cz8KCmBgYHtyLCBmaWcuaGVpZ2h0PTguOTUsIGZpZy53aWR0aD05Ljk1fQphbGwuc2FtcHMgPC0gdW5pcXVlKHBhc3RlKG1ldGEuZGYkQmxvY2ssIG1ldGEuZGYkQ29uZGl0aW9uLCBtZXRhLmRmJFJlcGxpY2F0ZSwgc2VwPSJfIikpCm1ldGEuZGYkQWxsLlNhbXBsZSA8LSBwYXN0ZShtZXRhLmRmJEJsb2NrLCBtZXRhLmRmJENvbmRpdGlvbiwgbWV0YS5kZiRSZXBsaWNhdGUsIHNlcD0iXyIpCmFsbC5jb3VudC5tYXRyaXggPC0gbWF0cml4KDBMLCBuY29sPWxlbmd0aChhbGwuc2FtcHMpLCBucm93PWxlbmd0aCh2ZXJ0ZXgubGlzdCkpCmNvbG5hbWVzKGFsbC5jb3VudC5tYXRyaXgpIDwtIGFsbC5zYW1wcwogIApmb3IoeCBpbiBzZXFfYWxvbmcoMTpsZW5ndGgodmVydGV4Lmxpc3QpKSl7CiAgdi54IDwtIHZlcnRleC5saXN0W1t4XV0KICBmb3IoaSBpbiBzZXFfYWxvbmcoMTpsZW5ndGgoYWxsLnNhbXBzKSkpewogICAgaS5zIDwtIGFsbC5zYW1wc1tpXQogICAgaS5zLnZlcnRpY2VzIDwtIGludGVyc2VjdCh2LngsIG1ldGEuZGZbbWV0YS5kZiRBbGwuU2FtcGxlID09IGkucywgXSRWZXJ0ZXgpCiAgICBhbGwuY291bnQubWF0cml4W3gsIGldIDwtIGxlbmd0aChpLnMudmVydGljZXMpCiAgfQp9CgphbGwuY291bnQubWVsdCA8LSBtZWx0KGFsbC5jb3VudC5tYXRyaXgpCmFsbC5jb3VudC5tZWx0JFZhcjIgPC0gYXMuY2hhcmFjdGVyKGFsbC5jb3VudC5tZWx0JFZhcjIpCmFsbC5jb3VudC5tZWx0JEJsb2NrIDwtIHVubGlzdChsYXBwbHkoc3Ryc3BsaXQoYWxsLmNvdW50Lm1lbHQkVmFyMiwgc3BsaXQ9Il8iLCBmaXhlZD1UUlVFKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBGVU49ZnVuY3Rpb24oWFApIHBhc3RlMChYUFsxXSkpKQphbGwuY291bnQubWVsdCRDb25kaXRpb24gPC0gdW5saXN0KGxhcHBseShzdHJzcGxpdChhbGwuY291bnQubWVsdCRWYXIyLCBzcGxpdD0iXyIsIGZpeGVkPVRSVUUpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEZVTj1mdW5jdGlvbihYUCkgcGFzdGUwKFhQWzJdKSkpCmFsbC5jb3VudC5tZWx0JFJlcGxpY2F0ZSA8LSB1bmxpc3QobGFwcGx5KHN0cnNwbGl0KGFsbC5jb3VudC5tZWx0JFZhcjIsIHNwbGl0PSJfIiwgZml4ZWQ9VFJVRSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRlVOPWZ1bmN0aW9uKFhQKSBwYXN0ZTAoWFBbM10pKSkKCmdncGxvdChhbGwuY291bnQubWVsdFthbGwuY291bnQubWVsdCRWYXIxICVpbiUgYyhuZWlnaGJvdXIubWVyZ2UkTmVpZ2hib3VyaG9vZFtuZWlnaGJvdXIubWVyZ2UkU2lnID09IDFdKSAmCiAgICAgICAgICAgICAgICAgICAgICAgIGFsbC5jb3VudC5tZWx0JEJsb2NrICVpbiUgYygiQjMiKSwgXSwKICAgICAgIGFlcyh4PUJsb2NrLCB5PXZhbHVlLCBmaWxsPUNvbmRpdGlvbikpICsKICBnZW9tX2JveHBsb3QoKSArCiAgdGhlbWVfY2xlYW4oKSArCiAgZmFjZXRfd3JhcCh+VmFyMSwgc2NhbGVzPSJmcmVlX3kiKQpgYGAKClRoZXNlIGFyZSB0aGUgY291bnRzIGZvciBCMyBpbiB0aGUgREFOcy4gTjIxIGxvb2tzIGxpa2UgdGhlIGVmZmVjdCBpcyBmcm9tIHNhbXBsaW5nIHZhcmlhbmNlLCBhcyB0aGUgY291bnRzIGFyZSByZWFsbHkgcXVpdGUgbG93LgoKIyMjIFRlc3QgdXNpbmcgcmVmaW5lZCBzYW1wbGluZwoKYGBge3J9CnJlZmluZV92ZXJ0ZXggPC0gZnVuY3Rpb24odmVydGV4Lmtubiwgdi5peCwgWF9wY2EpewogICMgdmVydGV4LmtubjogS05OIGdyYXBoIGZvciByYW5kb21seSBzYW1wbGVkIHBvaW50cyAob3V0cHV0IG9mIEJpb2NOZWlnaGJvcnM6OmZpbmRLTk4pCiAgIyB2Lml4OiBpbmRleCBvZiB2ZXJ0ZXggdG8gcmVmaW5lIGluIHZlcnRleC5rbm4KICAKICAjIyBDYWxjdWxhdGUgbWVkaWFuIHByb2ZpbGUgb2YgS05OcyBvZiB2ZXJ0ZXgKICB2Lm1lZCA8LSBhcHBseShYX3BjYVt2ZXJ0ZXgua25uJGluZGV4W3YuaXgsXSxdLCAyLCBtZWRpYW4pCiAgIyMgRmluZCB0aGUgY2xvc2VzdCBwb2ludCB0byB0aGUgbWVkaWFuIGFuZCBzYW1wbGUKICByZWZpbmVkLnZlcnRleCA8LSBCaW9jTmVpZ2hib3JzOjpmaW5kS05OKHJiaW5kKHYubWVkLCBYX3BjYSksIHN1YnNldD0xLCBrPTEpW1siaW5kZXgiXV1bMV0gLSAxICMjIC0xIHRvIHJlbW92ZSB0aGUgbWVkaWFuCiAgcmV0dXJuKHJlZmluZWQudmVydGV4KQp9CgoKcXVhbnRfbmVpZ2hib3VyaG9vZCA8LSBmdW5jdGlvbihncmFwaCwgbWV0YSwgc2FtcGxlLmNvbHVtbj0nU2FtcGxlJywgc2FtcGxlLnZlcnRpY2VzPTAuMjUsIHNlZWQ9NDIsIHBjYT1OVUxMLCBzYW1wbGU9InJhbmRvbSIpewogIHNldC5zZWVkKHNlZWQpCiAgCiAgaWYoc2FtcGxlID09ICJyYW5kb20iKXsKICAjIGRlZmluZSBhIHNldCBvZiB2ZXJ0aWNlcyBhbmQgbmVpaGJvdXJob29kIGNlbnRlcnMgLSBleHRyYWN0IHRoZSBuZWloYm91cmhvb2RzIG9mIHRoZXNlIGNlbGxzCiAgcmFuZG9tLnZlcnRpY2VzIDwtIHNhbXBsZShWKGdyYXBoKSwgc2l6ZT1mbG9vcihzYW1wbGUudmVydGljZXMqbGVuZ3RoKFYoZ3JhcGgpKSkpCiAgdmVydGV4Lmxpc3QgPC0gc2FwcGx5KDE6bGVuZ3RoKHJhbmRvbS52ZXJ0aWNlcyksIEZVTj1mdW5jdGlvbihYKSBuZWlnaGJvcnMoZ3JhcGgsIHY9cmFuZG9tLnZlcnRpY2VzW1hdKSkKICB9IGVsc2UgaWYoc2FtcGxlID09ICJyZWZpbmVkIil7CiAgICBpZihpcy5udWxsKHBjYSkpewogICAgICBzdG9wKCJQbGVhc2UgcGFzcyBhIFBDQSBvYmplY3QgLSBleHBlY3RlZCBvdXRwdXQgZnJvbSBwcmNvbXAoKSIpCiAgICB9CiAgICBYX3BjYSA8LSBwY2EkeFssIGMoMTozMCldCiAgICAKICAgIHJhbmRvbS52ZXJ0aWNlcyA8LSBzYW1wbGUoVihncmFwaCksIHNpemU9Zmxvb3Ioc2FtcGxlLnZlcnRpY2VzKmxlbmd0aChWKGdyYXBoKSkpKQogICAgdmVydGV4LmtubiA8LSBCaW9jTmVpZ2hib3JzOjpmaW5kS05OKFg9WF9wY2EsIGs9MjEsIHN1YnNldD1hcy52ZWN0b3IocmFuZG9tLnZlcnRpY2VzKSkKICAgIHJlZmluZWQudmVydGljZXMgPC0gVihncmFwaClbc2FwcGx5KDE6bnJvdyh2ZXJ0ZXgua25uJGluZGV4KSwgZnVuY3Rpb24oaSkgcmVmaW5lX3ZlcnRleCh2ZXJ0ZXgua25uLCBpLCBYX3BjYSkpXQogIAogICAgdmVydGV4Lmxpc3QgPC0gc2FwcGx5KDE6bGVuZ3RoKHJhbmRvbS52ZXJ0aWNlcyksIEZVTj1mdW5jdGlvbihYKSBuZWlnaGJvcnMoZ3JhcGgsIHY9cmFuZG9tLnZlcnRpY2VzW1hdKSkKICAgIHZlcnRleC5saXN0LnJlZmluZWQgPC0gc2FwcGx5KDE6bGVuZ3RoKHJlZmluZWQudmVydGljZXMpLCBGVU49ZnVuY3Rpb24oWCkgbmVpZ2hib3JzKGdyYXBoLCB2PXJlZmluZWQudmVydGljZXNbWF0pKSAgCiAgICB2ZXJ0ZXgubGlzdCA8LSB2ZXJ0ZXgubGlzdC5yZWZpbmVkCiAgfQogIAogIGNvdW50Lm1hdHJpeCA8LSBtYXRyaXgoMEwsIG5jb2w9bGVuZ3RoKHVuaXF1ZShtZXRhWywgc2FtcGxlLmNvbHVtbl0pKSwgbnJvdz1sZW5ndGgodmVydGV4Lmxpc3QpKQogIGNvbG5hbWVzKGNvdW50Lm1hdHJpeCkgPC0gdW5pcXVlKG1ldGFbLCBzYW1wbGUuY29sdW1uXSkKICAKICBmb3IoeCBpbiBzZXFfYWxvbmcoMTpsZW5ndGgodmVydGV4Lmxpc3QpKSl7CiAgICB2LnggPC0gdmVydGV4Lmxpc3RbW3hdXQogICAgZm9yKGkgaW4gc2VxX2Fsb25nKDE6bGVuZ3RoKHVuaXF1ZShtZXRhWywgc2FtcGxlLmNvbHVtbl0pKSkpewogICAgICBpLnMgPC0gdW5pcXVlKG1ldGFbLCBzYW1wbGUuY29sdW1uXSlbaV0KICAgICAgaS5zLnZlcnRpY2VzIDwtIGludGVyc2VjdCh2LngsIG1ldGFbbWV0YVssIHNhbXBsZS5jb2x1bW5dID09IGkucywgXSRWZXJ0ZXgpCiAgICAgIGNvdW50Lm1hdHJpeFt4LCBpXSA8LSBsZW5ndGgoaS5zLnZlcnRpY2VzKQogICAgfQogIH0KICByb3duYW1lcyhjb3VudC5tYXRyaXgpIDwtIGMoMTpsZW5ndGgodmVydGV4Lmxpc3QpKQogIHJldHVybihjb3VudC5tYXRyaXgpCn0KYGBgCgoKYGBge3J9CnNpbTIuY291bnRzIDwtIHF1YW50X25laWdoYm91cmhvb2QoZ3JhcGg9c2ltMi5rbm4sIG1ldGE9bWV0YS5kZiwgc2FtcGxlLmNvbHVtbj0nU2FtcGxlJywgc2FtcGxlLnZlcnRpY2VzPW4uaG9vZCwgc2FtcGxlPSJyZWZpbmVkIiwgcGNhPXNpbTIucGNhKQpzYW1wbGUubWV0YSA8LSBkYXRhLmZyYW1lKCJDb25kaXRpb24iPWMocmVwKCJBIiwgMyksIHJlcCgiQiIsIDMpKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAiUmVwbGljYXRlIj1yZXAoYygiUjEiLCAiUjIiLCAiUjMiKSwgMikpCnNhbXBsZS5tZXRhJFNhbXBsZSA8LSBwYXN0ZShzYW1wbGUubWV0YSRDb25kaXRpb24sIHNhbXBsZS5tZXRhJFJlcGxpY2F0ZSwgc2VwPSJfIikKcm93bmFtZXMoc2FtcGxlLm1ldGEpIDwtIHNhbXBsZS5tZXRhJFNhbXBsZQojIHNpbTIubW9kZWwgPC0gbW9kZWwubWF0cml4KH4gMCArIENvbmRpdGlvbiwgZGF0YT1zYW1wbGUubWV0YSkKc2ltMi5tb2RlbCA8LSBtb2RlbC5tYXRyaXgofiBDb25kaXRpb24sIGRhdGE9c2FtcGxlLm1ldGEpCmhlYWQoc2ltMi5tb2RlbCkKYGBgCgpJIGhhdmUgYSBtb2RlbCBtYXRyaXggYW5kIGNvdW50cyBtYXRyaXggLSBsZXQncyB0ZXN0IGVkZ2VSIG9uIHRoZXNlLgoKYGBge3J9CnNpbTIuZGdlIDwtIERHRUxpc3Qoc2ltMi5jb3VudHNbLCByb3duYW1lcyhzaW0yLm1vZGVsKV0sIGxpYi5zaXplPWxvZyhjb2xTdW1zKHNpbTIuY291bnRzWywgcm93bmFtZXMoc2ltMi5tb2RlbCldKSkpCnNpbTIuZGdlIDwtIGVzdGltYXRlRGlzcChzaW0yLmRnZSwgc2ltMi5tb2RlbCwgdGFnd2lzZT1UUlVFKQpzaW0yLmZpdCA8LSBnbG1RTEZpdChzaW0yLmRnZSwgc2ltMi5tb2RlbCwgcm9idXN0PVRSVUUpCiMgc2ltMi5jb250cmFzdCA8LSBtYWtlQ29udHJhc3RzKENvbmRpdGlvbkEgLSBDb25kaXRpb25CLCBsZXZlbHM9c2ltMi5tb2RlbCkKIyBzaW0yLnJlcyA8LSBnbG1RTEZUZXN0KHNpbTIuZml0LCBjb250cmFzdD1zaW0yLmNvbnRyYXN0KQoKc2ltMi5yZXMgPC0gYXMuZGF0YS5mcmFtZSh0b3BUYWdzKGdsbVFMRlRlc3Qoc2ltMi5maXQsIGNvZWY9MiksIHNvcnQuYnk9J25vbmUnLCBuPUluZikpCnNpbTIucmVzJE5laWdoYm91cmhvb2QgPC0gYXMubnVtZXJpYyhyb3duYW1lcyhzaW0yLnJlcykpCgpzaW0yLnNwYXRpYWxmZHIgPC0gZ3JhcGhfc3BhdGlhbEZEUihuZWlnaGJvcmhvb2RzPXZlcnRleC5saXN0LnJlZmluZWQsIGdyYXBoPXNpbTIua25uLCBjb25uZWN0aXZpdHk9ImRpc3RhbmNlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHZhbHVlcz1zaW0yLnJlc1tvcmRlcihzaW0yLnJlcyROZWlnaGJvdXJob29kKSwgXSRQVmFsdWUsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwY2E9c2ltMi5wY2EkeFssIGMoMTozMCldKQoKc2ltMi5yZXMkU3BhdGlhbEZEUltvcmRlcihzaW0yLnJlcyROZWlnaGJvdXJob29kKV0gPC0gc2ltMi5zcGF0aWFsZmRyCnF2YWxzIDwtIHNpbTIuc3BhdGlhbGZkcgppcy5zaWcgPC0gcXZhbHMgPD0gMC4wMQpzdW1tYXJ5KGlzLnNpZykKYGBgCgpUaGF0J3MgYSBsb3Qgb2YgREEgbmVpZ2Jib3VyaG9vZHMhCgpgYGB7cn0Kc2ltMi5uZWlnaGJvdXIuZXhwcnMgPC0gbmVpZ2hib3Job29kX2V4cHJlc3Npb24odmVydGV4Lmxpc3QucmVmaW5lZCwgc2ltMi5nZXgpCmBgYAoKRW1iZWQgdGhlc2UgaHlwZXJzcGhlcmVzIHdpdGggYSBQQ0EgYW5kIFVNQVAuCgpgYGB7cn0Kc2ltMi5uZWlnaGJvdXIucGNhIDwtIHByY29tcCgodChzaW0yLm5laWdoYm91ci5leHBycykpKQpgYGAKCgpgYGB7cn0Kc2V0LnNlZWQoNDIpCm5laWdoYm91cmhvb2QudW1hcCA8LSB1bWFwKHNpbTIubmVpZ2hib3VyLnBjYSR4WywgYygxOjMwKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG5fY29tcG9uZW50cz0yLAogICAgICAgICAgICAgICAgICAgICAgICAgICBuX25laWdoYm9ycz0yMSwgbWV0cmljPSdldWNsaWRlYW4nLAogICAgICAgICAgICAgICAgICAgICAgICAgICBpbml0PSdyYW5kb20nLCBtaW5fZGlzdD0wLjEpCnBsb3QobmVpZ2hib3VyaG9vZC51bWFwJGxheW91dCwKICAgICB4bGFiPSJVTUFQIDEiLCB5bGFiPSJVTUFQIDIiKQpgYGAKCldlIGNhbiBvdmVybGF5IHRoZSBEQSB0ZXN0aW5nIG9uIHRoZXNlIG5laWdoYm91cmhvb2RzLgoKYGBge3J9Cm5laWdoYm9yLmRmIDwtIHNpbTIucmVzWywgYygibG9nRkMiLCAiTmVpZ2hib3VyaG9vZCIsICJTcGF0aWFsRkRSIildCm5laWdoYm9yLmRmIDwtIGRvLmNhbGwoY2JpbmQuZGF0YS5mcmFtZSwgbGlzdChuZWlnaGJvci5kZiwgYXMuZGF0YS5mcmFtZShuZWlnaGJvdXJob29kLnVtYXAkbGF5b3V0KSkpCmNvbG5hbWVzKG5laWdoYm9yLmRmKSA8LSBjKCJsb2dGQyIsICJOZWlnaGJvdXJob29kIiwgIlNwYXRpYWxGRFIiLCAiVU1BUDEiLCAiVU1BUDIiKQpuZWlnaGJvci5kZiRTaWcgPC0gYXMubnVtZXJpYyhuZWlnaGJvci5kZiRTcGF0aWFsRkRSIDw9IDAuMDEpCgpnZ3Bsb3QobmVpZ2hib3IuZGYsIGFlcyh4PVVNQVAxLCB5PVVNQVAyKSkgKwogIGdlb21fcG9pbnQoZGF0YT1uZWlnaGJvci5kZltuZWlnaGJvci5kZiRTaWcgPT0gMCwgXSwKICAgICAgICAgICAgIGNvbG91cj0nZ3JleTgwJywgc2l6ZT0yKSArCiAgZ2VvbV9wb2ludChkYXRhPW5laWdoYm9yLmRmW25laWdoYm9yLmRmJFNpZyA9PSAxLCBdLAogICAgICAgICAgICAgYWVzKGNvbG91cj1sb2dGQyksIHNpemU9NCkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9ncmFkaWVudDIobG93PSJibHVlIiwgbWlkPSJncmV5ODAiLCBoaWdoPSJyZWQiKQpgYGAKCk5vIG1vcmUgZmFsc2UgREFOcyBpbiBCMywgZGVzcGl0ZSB0aGUgc2FtZSBudW1iZXIgb2YgbmVpZ2hib3VyaG9vZHMgYmVpbmcgY291bnRlZC4KCmBgYHtyfQpuZWlnaGJvdXIubGlzdCA8LSBsaXN0KCkKZm9yKHggaW4gc2VxX2Fsb25nKDE6bGVuZ3RoKHZlcnRleC5saXN0LnJlZmluZWQpKSl7CiAgeC5kZiA8LSBtZXRhLmRmW21ldGEuZGYkVmVydGV4ICVpbiUgdmVydGV4Lmxpc3QucmVmaW5lZFtbeF1dLCBdCiAgeC5yZXAgPC0gbmFtZXModGFibGUoeC5kZiRSZXBsaWNhdGUpKVt3aGljaCh0YWJsZSh4LmRmJFJlcGxpY2F0ZSkgPT0gbWF4KHRhYmxlKHguZGYkUmVwbGljYXRlKSkpXQogIGlmKGxlbmd0aCh4LnJlcCkgPiAxKXsKICAgIHgucmVwIDwtIHNhbXBsZShzaXplPTEsIHgucmVwKQogIH0KICB4LmJsb2NrIDwtIG5hbWVzKHRhYmxlKHguZGYkQmxvY2spKVt3aGljaCh0YWJsZSh4LmRmJEJsb2NrKSA9PSBtYXgodGFibGUoeC5kZiRCbG9jaykpKV0KICAgIGlmKGxlbmd0aCh4LmJsb2NrKSA+IDEpewogICAgeC5ibG9jayA8LSBzYW1wbGUoc2l6ZT0xLCB4LmJsb2NrKQogIH0KICB4LmNvbmRpdGlvbiA8LSBuYW1lcyh0YWJsZSh4LmRmJENvbmRpdGlvbikpW3doaWNoKHRhYmxlKHguZGYkQ29uZGl0aW9uKSA9PSBtYXgodGFibGUoeC5kZiRDb25kaXRpb24pKSldCiAgICBpZihsZW5ndGgoeC5jb25kaXRpb24pID4gMSl7CiAgICB4LmNvbmRpdGlvbiA8LSBzYW1wbGUoc2l6ZT0xLCB4LmNvbmRpdGlvbikKICB9CiAgCiAgbmVpZ2hib3VyLmxpc3RbW3hdXSA8LSBkYXRhLmZyYW1lKCJSZXBsaWNhdGUiPXgucmVwLCAiQmxvY2siPXguYmxvY2ssICJDb25kaXRpb24iPXguY29uZGl0aW9uLCAiTmVpZ2hib3VyaG9vZCI9eCkKfQoKbmVpZ2hib3VyLm1ldGEgPC0gZG8uY2FsbChyYmluZC5kYXRhLmZyYW1lLCBuZWlnaGJvdXIubGlzdCkKbmVpZ2hib3VyLm1lcmdlIDwtIG1lcmdlKG5laWdoYm9yLmRmLCBuZWlnaGJvdXIubWV0YSwgYnk9J05laWdoYm91cmhvb2QnKQpuZWlnaGJvdXIubWVyZ2UkQmxvY2sgPC0gb3JkZXJlZChuZWlnaGJvdXIubWVyZ2UkQmxvY2ssCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxldmVscz1jKCJCMSIsICJCMyIpKQpuZWlnaGJvdXIubWVyZ2UkRGlmZiA8LSBzaWduKG5laWdoYm91ci5tZXJnZSRsb2dGQykKbmVpZ2hib3VyLm1lcmdlJERpZmZbbmVpZ2hib3VyLm1lcmdlJFNpZyA9PSAwXSA8LSAwCmBgYAoKCmBgYHtyLCBmaWcud2lkdGg9OS43NSwgZmlnLmhlaWdodD00LjE1fQpnZ3Bsb3QobmVpZ2hib3VyLm1lcmdlLCBhZXMoeD1VTUFQMSwgeT1VTUFQMikpICsKICBnZW9tX3BvaW50KGRhdGE9bmVpZ2hib3VyLm1lcmdlWywgYygiVU1BUDEiLCAiVU1BUDIiKV0sCiAgICAgICAgICAgICBjb2xvdXI9J2dyZXk4MCcsIHNpemU9MSkgKwogIGdlb21fcG9pbnQoZGF0YT1uZWlnaGJvdXIubWVyZ2VbbmVpZ2hib3VyLm1lcmdlJFNpZyA9PSAxLCBdLAogICAgICAgICAgICAgYWVzKGNvbG91cj1sb2dGQyksIHNpemU9NCkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG91cl9ncmFkaWVudDIobG93PSJibHVlIiwgbWlkPSJncmV5ODAiLCBoaWdoPSJyZWQiKSArCiAgZmFjZXRfd3JhcCh+QmxvY2spCmBgYAoKVGhpcyBkb2Vzbid0IG1ha2Ugc2Vuc2UgLSBCbG9jayAzIHNob3VsZG4ndCBoYXZlIGFueSBEQSBuZWlnaGJvdXJob29kcy4gSXMgdGhpcyBhIGNvbXBvc2l0aW9uYWwgZWZmZWN0IHdlJ3JlIHNlZWluZyBoZXJlPyBJdCdzIHN0cmFuZ2UgCnRoYXQgYSByYW5kb20gZmx1Y3R1YXRpb24gd291bGQgY2F1c2UgdGhpcyAtIGl0IG11c3QgYmUgaW5jcmVkaWJseSBzZW5zaXRpdmUuCgpgYGB7cn0KdGFibGUobmVpZ2hib3VyLm1lcmdlJEJsb2NrLCBuZWlnaGJvdXIubWVyZ2UkRGlmZikKYGBgCgpgYGB7ciwgZmlnLmhlaWdodD04Ljk1LCBmaWcud2lkdGg9OS45NX0KYWxsLnNhbXBzIDwtIHVuaXF1ZShwYXN0ZShtZXRhLmRmJEJsb2NrLCBtZXRhLmRmJENvbmRpdGlvbiwgbWV0YS5kZiRSZXBsaWNhdGUsIHNlcD0iXyIpKQptZXRhLmRmJEFsbC5TYW1wbGUgPC0gcGFzdGUobWV0YS5kZiRCbG9jaywgbWV0YS5kZiRDb25kaXRpb24sIG1ldGEuZGYkUmVwbGljYXRlLCBzZXA9Il8iKQphbGwuY291bnQubWF0cml4IDwtIG1hdHJpeCgwTCwgbmNvbD1sZW5ndGgoYWxsLnNhbXBzKSwgbnJvdz1sZW5ndGgodmVydGV4Lmxpc3QucmVmaW5lZCkpCmNvbG5hbWVzKGFsbC5jb3VudC5tYXRyaXgpIDwtIGFsbC5zYW1wcwogIApmb3IoeCBpbiBzZXFfYWxvbmcoMTpsZW5ndGgodmVydGV4Lmxpc3QucmVmaW5lZCkpKXsKICB2LnggPC0gdmVydGV4Lmxpc3QucmVmaW5lZFtbeF1dCiAgZm9yKGkgaW4gc2VxX2Fsb25nKDE6bGVuZ3RoKGFsbC5zYW1wcykpKXsKICAgIGkucyA8LSBhbGwuc2FtcHNbaV0KICAgIGkucy52ZXJ0aWNlcyA8LSBpbnRlcnNlY3Qodi54LCBtZXRhLmRmW21ldGEuZGYkQWxsLlNhbXBsZSA9PSBpLnMsIF0kVmVydGV4KQogICAgYWxsLmNvdW50Lm1hdHJpeFt4LCBpXSA8LSBsZW5ndGgoaS5zLnZlcnRpY2VzKQogIH0KfQoKYWxsLmNvdW50Lm1lbHQgPC0gbWVsdChhbGwuY291bnQubWF0cml4KQphbGwuY291bnQubWVsdCRWYXIyIDwtIGFzLmNoYXJhY3RlcihhbGwuY291bnQubWVsdCRWYXIyKQphbGwuY291bnQubWVsdCRCbG9jayA8LSB1bmxpc3QobGFwcGx5KHN0cnNwbGl0KGFsbC5jb3VudC5tZWx0JFZhcjIsIHNwbGl0PSJfIiwgZml4ZWQ9VFJVRSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRlVOPWZ1bmN0aW9uKFhQKSBwYXN0ZTAoWFBbMV0pKSkKYWxsLmNvdW50Lm1lbHQkQ29uZGl0aW9uIDwtIHVubGlzdChsYXBwbHkoc3Ryc3BsaXQoYWxsLmNvdW50Lm1lbHQkVmFyMiwgc3BsaXQ9Il8iLCBmaXhlZD1UUlVFKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBGVU49ZnVuY3Rpb24oWFApIHBhc3RlMChYUFsyXSkpKQphbGwuY291bnQubWVsdCRSZXBsaWNhdGUgPC0gdW5saXN0KGxhcHBseShzdHJzcGxpdChhbGwuY291bnQubWVsdCRWYXIyLCBzcGxpdD0iXyIsIGZpeGVkPVRSVUUpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEZVTj1mdW5jdGlvbihYUCkgcGFzdGUwKFhQWzNdKSkpCgpnZ3Bsb3QoYWxsLmNvdW50Lm1lbHRbYWxsLmNvdW50Lm1lbHQkVmFyMSAlaW4lIGMobmVpZ2hib3VyLm1lcmdlJE5laWdoYm91cmhvb2RbbmVpZ2hib3VyLm1lcmdlJFNpZyA9PSAxXSkgJgogICAgICAgICAgICAgICAgICAgICAgICBhbGwuY291bnQubWVsdCRCbG9jayAlaW4lIGMoIkIzIiksIF0sCiAgICAgICBhZXMoeD1CbG9jaywgeT12YWx1ZSwgZmlsbD1Db25kaXRpb24pKSArCiAgZ2VvbV9ib3hwbG90KCkgKwogIHRoZW1lX2NsZWFuKCkgKwogIGZhY2V0X3dyYXAoflZhcjEsIHNjYWxlcz0iZnJlZV95IikKYGBgCgoKCiMgTmV3IGJ1aWxkS05OR3JhcGgoKSB0byByZXRhaW4gY2VsbC1jZWxsIGRpc3RhbmNlcwoKSWYgd2Ugd2FudCB0byBiYXNlIHRoZSBzcGF0aWFsIEZEUiBjb3JyZWN0aW9uIG9uIGEgZGlzdGFuY2UgdGhlbiB3ZSBuZWVkIHNvbWUgd2F5IHRvIHJldGFpbiB0aGlzIGluZm9ybWF0aW9uIGluIHRoZSBncmFwaCB0byBzcGVlZC11cCB0aGUgY2FsY3VsYXRpb25zIGZvciAKbGFyZ2UgYW5kIGhpZ2hseS1jb25uZWN0ZWQgZ3JhcGhzLiBPdXIgaWRlYSBpcyB0byByZS1jb2RlIHRoZSBgYnVpbGRLTk5HcmFwaCgpYCBmdW5jdGlvbiB0byByZXRhaW4gY2VsbC1jZWxsIGRpc3RhbmNlcyBpbiB0aGUgZWRnZSB3ZWlnaHRzIHNsb3Qgb2YgdGhlIAppZ3JhcGggb2JqZWN0LgoKYGBge3IsIGVjaG89RkFMU0UsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9CiMgcmVxdWlyZShCaW9jTmVpZ2hib3JzKQojIAojIG5laWdoYm9yc1RvS05OR3JhcGggPC0gZnVuY3Rpb24obm4sIGRpcmVjdGVkPUZBTFNFLCBkaXN0YW5jZXM9RkFMU0UpIHsKIyAgIGluZGljZXMgPC0gbm4kaW5kZXgKIyAgIHN0YXJ0IDwtIGFzLnZlY3Rvcihyb3coaW5kaWNlcykpCiMgICBlbmQgPC0gYXMudmVjdG9yKGluZGljZXMpCiMgICBpbnRlcmxlYXZlZCA8LSBhcy52ZWN0b3IocmJpbmQoc3RhcnQsIGVuZCkpCiMgICAKIyAgIAojICAgaWYgKGRpcmVjdGVkKSB7CiMgICAgIGcgPC0gaWdyYXBoOjptYWtlX2dyYXBoKGludGVybGVhdmVkLCBkaXJlY3RlZD1UUlVFKQojICAgICAKIyAgICAgaWYoaXNGQUxTRShkaXN0YW5jZXMpKXsKIyAgICAgICBFKGcpJHdlaWdodCA8LSBOQQojICAgICB9IGVsc2V7CiMgICAgICAgZGlzdHMgPC0gYXMuZGF0YS5mcmFtZShtZWx0KG5uJGRpc3RhbmNlKSkKIyAgICAgICBjb2xuYW1lcyhkaXN0cykgPC0gYygiZnJvbSIsICJ0byIsICJ3ZWlnaHQiKQojICAgICAgICMgcmVtb3ZlIGVkZ2VzIHRoYXQgYXJlIG11bHRpcGxlcwojICAgICAgIGQuZGYgPC0gZ3JhcGguZGF0YS5mcmFtZShkaXN0cykKIyAgICAgICBFKGcpJHdlaWdodCA8LSBkLmRmCiMgICAgIAojICAgICB9CiMgICB9IGVsc2UgewojICAgICBnIDwtIGlncmFwaDo6bWFrZV9ncmFwaChpbnRlcmxlYXZlZCwgZGlyZWN0ZWQ9RkFMU0UpCiMgICAgIAojICAgICBpZihpc0ZBTFNFKGRpc3RhbmNlcykpewojICAgICAgIEUoZykkd2VpZ2h0IDwtIE5BCiMgICAgIH0gZWxzZXsKIyAgICAgICBkaXN0cyA8LSBhcy5kYXRhLmZyYW1lKG1lbHQobm4kZGlzdGFuY2UpKQojICAgICAgIGNvbG5hbWVzKGRpc3RzKSA8LSBjKCJmcm9tIiwgInRvIiwgIndlaWdodCIpCiMgICAgICAgZC5kZiA8LSBncmFwaC5kYXRhLmZyYW1lKGRpc3RzKQojICAgICAgIEUoZykkd2VpZ2h0IDwtIGQuZGYKIyAgICAgfQojICAgICAgIAojICAgICBnIDwtIGlncmFwaDo6c2ltcGxpZnkoZywgZWRnZS5hdHRyLmNvbWIgPSAiZmlyc3QiKQojICAgICB9CiMgICBnCiMgfQojIAojIAojIC5idWlsZEtOTkdyYXBoIDwtIGZ1bmN0aW9uKHgsIGs9MTAsIGQ9NTAsIGRpcmVjdGVkPUZBTFNFLCB0cmFuc3Bvc2VkPUZBTFNFLCBrZWVwLmRpc3RhbmNlPUZBTFNFLAojICAgICBzdWJzZXQucm93PU5VTEwsIEJOUEFSQU09S21rbm5QYXJhbSgpLCBCU1BBUkFNPWJzcGFyYW0oKSwgQlBQQVJBTT1TZXJpYWxQYXJhbSgpKQojICAgewojICAgIyB0aGlzIHdpbGwgbm93IHJldGFpbiB0aGUgZGlzdGFuY2VzIHRvIG5laWdoYm91cnMgLSBuZWVkIHRvIHBhc3MgdGhpcyB0byBgbmVpZ2hib3JzVG9LTk5HcmFwaGAKIyAgIG5uLm91dCA8LSAuc2V0dXBfa25uX2RhdGEoeD14LCBzdWJzZXQucm93PXN1YnNldC5yb3csIGQ9ZCwgdHJhbnNwb3NlZD10cmFuc3Bvc2VkLCBrZWVwLmRpc3RhbmNlPWtlZXAuZGlzdGFuY2UsCiMgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGs9aywgQk5QQVJBTT1CTlBBUkFNLCBCU1BBUkFNPUJTUEFSQU0sIEJQUEFSQU09QlBQQVJBTSkKIyAgIHNpbmsoZmlsZT0iL2Rldi9udWxsIikKIyAgIGdjKCkKIyAgIHNpbmsoZmlsZT1OVUxMKQojICAgIyB0ZXN0IGZvciBwcmVzZW5jZSBvZiBkaXN0YW5jZXMKIyAgIG5laWdoYm9yc1RvS05OR3JhcGgobm4ub3V0LCBkaXJlY3RlZD1kaXJlY3RlZCwgZGlzdGFuY2VzPWtlZXAuZGlzdGFuY2UpCiMgfQojIAojIAojICMnIEBpbXBvcnRGcm9tIEJpb2NOZWlnaGJvcnMgZmluZEtOTgojIC5zZXR1cF9rbm5fZGF0YSA8LSBmdW5jdGlvbih4LCBzdWJzZXQucm93LCBkLCB0cmFuc3Bvc2VkLCBrLCBCTlBBUkFNLCBCU1BBUkFNLCBCUFBBUkFNLCBrZWVwLmRpc3RhbmNlPWtlZXAuZGlzdGFuY2UpIHsKIyAgICAgaWYgKCF0cmFuc3Bvc2VkKSB7CiMgICAgICAgICBpZiAoIWlzLm51bGwoc3Vic2V0LnJvdykpIHsKIyAgICAgICAgICAgICB4IDwtIHhbc3Vic2V0LnJvdywsZHJvcD1GQUxTRV0KIyAgICAgICAgIH0KIyAgICAgICAgIHggPC0gdCh4KQojICAgICB9IAojICAgICAKIyAgICAgIyBSZWR1Y2luZyBkaW1lbnNpb25zLCBpZiAnZCcgaXMgbGVzcyB0aGFuIHRoZSBudW1iZXIgb2YgZ2VuZXMuCiMgICAgIGlmICghaXMubmEoZCkgJiYgZCA8IG5jb2woeCkpIHsKIyAgICAgICAgIHN2ZC5vdXQgPC0gLmNlbnRlcmVkX1NWRCh4LCBtYXgucmFuaz1kLCBrZWVwLnJpZ2h0PUZBTFNFLCBCU1BBUkFNPUJTUEFSQU0sIEJQUEFSQU09QlBQQVJBTSkKIyAgICAgICAgIHggPC0gLnN2ZF90b19wY2Eoc3ZkLm91dCwgZCwgbmFtZWQ9RkFMU0UpCiMgICAgIH0KIyAgICAKIyAgICAgIyBGaW5kaW5nIHRoZSBLTk5zIC0ga2VlcCB0aGUgZGlzdGFuY2VzCiMgICAgIGZpbmRLTk4oeCwgaz1rLCBCTlBBUkFNPUJOUEFSQU0sIEJQUEFSQU09QlBQQVJBTSwgZ2V0LmRpc3RhbmNlPWtlZXAuZGlzdGFuY2UpCiMgfQpgYGAKCgpgYGB7cn0KIyBzZXQuc2VlZCg0MikKIyBzaW0yLmtubiA8LSAuYnVpbGRLTk5HcmFwaCh4PXNpbTIucGNhJHhbLCBjKDE6MzApXSwgaz0yMSwgZD1OQSwgdHJhbnNwb3NlZD1UUlVFKQojIHNpbTIuZnIubGF5b3V0IDwtIGxheW91dF93aXRoX2ZyKHNpbTIua25uKQojIHBsb3Qoc2ltMi5rbm4sIGxheW91dD1zaW0yLmZyLmxheW91dCwgdmVydGV4LmZyYW1lLmNvbG9yPSdza3libHVlJywgdmVydGV4LmNvbG9yPSdza3libHVlJywgdmVydGV4LmxhYmVsLmNvbG9yPSdibGFjaycsCiMgICAgICB2ZXJ0ZXgubGFiZWwuZmFtaWx5PSdIZWx2ZXRpY2EnLCBlZGdlLmNvbG9yPSdncmV5NjAnLCB2ZXJ0ZXgubGFiZWwuY2V4PTAuOSwKIyAgICAgIHZlcnRleC5sYWJlbC5kaXN0PTEsIGVkZ2UuYXJyb3cuc2l6ZT0wLjIpCmBgYAoKX19OQl9fOiBTdG9yaW5nIHRoZSBkaXN0YW5jZXMgYXMgZWRnZSB3ZWlnaHRzIGlzIGZhciB0b28gbWVtb3J5IGludGVuc2l2ZS4gUGVyaGFwcyB3b3JraW5nIG9mZiB0aGUgYmFjayBvZiB0aGUgU2luZ2xlQ2VsbEV4cGVyaW1lbnQgd291bGQgYmUgYmV0dGVyIGZyb20gYSAKZGVzaWduIHBvaW50IG9mIHZpZXcuCgoKCgo=
Milo - simulated trajectory graphs
library(ggplot2)
library(SingleCellExperiment)
library(tidyverse)
library(igraph)
library(scran)

devtools::load_all("~/milo/miloR/")

Testing with random sampling

Simulate linear trajectory

Using dyntoy, I simulate a scRNA-seq data with cells forming a linear trajectory (no branches) with 5000 cells and 10 main states (milestones).

set.seed(42)
dataset <- generate_dataset(
  model = model_linear(num_milestones = 10),
  num_cells = 5000,
  num_features = 5000
)

|===============================================================================     | 95% ~0 s remaining     
|=================================================================================   | 97% ~0 s remaining     
|=================================================================================== | 99% ~0 s remaining     
gex <- as.matrix(dataset$expression)
branches <- dataset$prior_information$groups_id
  
## Dimensionality reduction
pca <- prcomp_irlba(gex, n=50, scale.=TRUE, center=TRUE)
X_pca = pca$x[, c(1:30)]

I assign cells to simulated biological conditions and replicates, that we will use for differential abundance testing. For each of the \(M\) clusters, I assign different proportions of cells to condition A or condition B, while simulating proportionate mixing between replicates.

Construct a Milo object

sim_milo
class: Milo 
dim: 5000 5000 
metadata(0):
assays(2): counts logcounts
rownames(5000): G1 G2 ... G4999 G5000
rowData names(0):
colnames(5000): C1 C2 ... C4999 C5000
colData names(4): group_id condition replicate sample
reducedDimNames(1): PCA
altExpNames(0):
neighbourhoods dimensions(1): 0
neighbourhoodCounts dimensions(2): 1 1
neighbourDistances dimensions(2): 1 1
graph names(0):
neighbourhoodIndex names(1): 0

Build KNN-graph

sim_milo <- buildGraph(sim_milo, k = 20, d = 30)
Constructing kNN graph with k:20
Retrieving distances from 20 nearest neighbours

Make neighbourhoods

Using sampling of 10% random points

Count cells within neighbourhoods

sim_milo <- countCells(sim_milo, data = data.frame(colData(sim_milo)), samples = "sample")
Checking data validity
Setting up matrix with 500 neighbourhoods
Counting cells in neighbourhoods
head(neighbourhoodCounts(sim_milo))
6 x 6 sparse Matrix of class "dgCMatrix"
  B_R1 A_R1 B_R2 A_R2 A_R3 B_R3
1    2    7    2    3    4    2
2   11    5   12    3    5   12
3    9    7   11    3    5    6
4    7    2    3    .    1   11
5    .    7    2    9    4    1
6    3    3    5    5    5    3

Test for DA

Visualize results in embedding

Refined sampling scheme

We adopt the refined sampling strategy applied in Wishbone, and adapted from here. Briefly, to avoid selecting outliers with random sampling, I first randomly select \(n\) cells. For each sampled cell I then identify its k neares neighbors and compute the median profile of the neighbors (in this case the profile in reduced PC space). Then I replace each sampled cell by the cell closest to the median profile of its neighbors.

sim_milo_ref <- makeNeighbourhoods(sim_milo,prop = 0.1, k=20, d=30, refined = TRUE)
Checking valid object

Refined sampling stats

With the refined sampling scheme I select cells with a larger neighbourhood on average.

When \(n\) is large I often end up sampling less than \(n\) cells because for many randomly sampled cells the cell closest to the KNNs is the same.

seq(0.01,0.5, by = 0.05)
 [1] 0.01 0.06 0.11 0.16 0.21 0.26 0.31 0.36 0.41 0.46

Compare DA testing results

res_ref <- testNeighbourhoods(sim_milo_ref, ~ 1 + condition, data = design_df, fdr.weighting = "k-distance")
Performing spatial FDR correction

Refined sampling seems to be able to identify DA at both ends of the spectrum better

plotMiloReducedDim(sim_milo, res_rand, pt_size=2) + ggtitle("Random sampling") 

plotMiloReducedDim(sim_milo_ref, res_ref, pt_size=2) + ggtitle("Refined sampling")

As expected multiple testing correction is less severe with the refined sample set (less points)

res_rand %>%
  ggplot(aes(-log10(PValue), -log10(SpatialFDR))) +
  geom_abline(linetype=2) +
  geom_point() +
  ggtitle("Refined sampling") 

res_ref %>%
  ggplot(aes(-log10(PValue), -log10(SpatialFDR))) +
  geom_abline(linetype=2) +
  ggtitle("Random sampling") +
  geom_point() 

Picking sampling proportion and k

I want to select these parameters to increase mean nh size


Old code

Robustness of test outcomes

I want to check whether using refined sampling allows to have more logFC even with different sampling

mean_nh_sizes
  [1]  28.64286  33.20732  35.25000  38.78472  38.53125  49.94444  62.41176  64.72549  74.22481  74.93976  63.75862  83.53968  90.72917
 [14] 100.84348 106.32168  75.80000 100.18644 115.23077 124.29630 131.64085  84.44000 116.83051 136.33333 149.01010 160.13846  27.04762
 [27]  30.27891  34.40984  35.60887  35.00300  48.63158  57.05983  64.41975  65.90868  68.34657  65.31111  78.37963  89.54015  93.79104
 [40]  99.00800  76.78049  96.50000 112.80488 120.74011 125.87391  84.68293 112.50000 131.10000 143.05357 150.30097  26.48810  31.16949
 [53]  31.85039  32.59831  33.01573  46.52055  55.45946  60.25123  61.62791  64.67363  60.72414  75.69173  85.32955  88.09506  92.12575
 [66]  71.59259  94.60484 108.88387 112.68333 119.77517  80.38636 111.71287 128.65248 136.46635 144.13910  25.00980  28.42035  31.47619
 [79]  31.28369  32.08755  45.50562  52.29775  58.47490  59.66289  62.16317  60.88889  74.37931  84.51402  85.57413  90.45431  72.01471
 [92]  92.57812 107.51020 110.75746 115.67049  80.37288 106.41739 126.79651 133.06276 142.97603  26.21552  28.05512  31.16071  30.49407
[105]  31.15677  44.42105  52.66497  58.34082  56.54884  60.65984  59.76471  74.73171  83.37885  83.07163  87.92148  69.76000  92.60000
[118] 105.17647 108.30323 112.15365  79.95161 110.25210 126.00546 131.74170 138.67422
run_milo_sampling <- function(graph, meta.df, model, X_pca, seed=42, sample.vertices=0.1){
  set.seed(seed)
  random.vertices <- sample(V(graph), size=floor(sample.vertices*length(V(graph))))
  vertex.knn <- BiocNeighbors::findKNN(X=X_pca, k=21, subset=as.vector(random.vertices))
  refined.vertices <- V(graph)[sapply(1:nrow(vertex.knn$index), function(i) refine_vertex(vertex.knn, i, X_pca))]
  
  vertex.list <- sapply(1:length(random.vertices), FUN=function(X) neighbors(graph, v=random.vertices[X]))
  vertex.list.refined <- sapply(1:length(refined.vertices), FUN=function(X) neighbors(graph, v=refined.vertices[X]))
  
  count.matrix.random <- countCells(sim2.knn, meta.df, vertex.list = vertex.list, random.vertices = random.vertices, sample.column = "sample")
  count.matrix.refined <- countCells(sim2.knn, meta.df, vertex.list = vertex.list.refined, random.vertices = refined.vertices, sample.column = "sample")
    
  spFDR.random <- testQLF(graph, count.matrix.random, model)
  spFDR.refined <- testQLF(graph, count.matrix.refined, model)
  
  fdr.df.random <- data.frame(Vertex=as.integer(rownames(spFDR.random$res)), p=spFDR.random$res$PValue, adjp=spFDR.random$spFDR, adjp_fdr=spFDR.random$res$FDR, logFC=spFDR.random$res$logFC, Sig=spFDR.random$res$Sig)
  fdr.df.refined <- data.frame(Vertex=as.integer(rownames(spFDR.refined$res)), p=spFDR.refined$res$PValue, adjp=spFDR.refined$spFDR, logFC=spFDR.refined$res$logFC, adjp_fdr=spFDR.refined$res$FDR, Sig=spFDR.refined$res$Sig)

  return(list(random=fdr.df.random, refined=fdr.df.refined))
}

sample_perc5 <- map(2020:2025, ~ run_milo_sampling(data_5k_cells$graph, data_5k_cells$meta.df, data_5k_cells$model, data_5k_cells$X_pca, seed=.x, sample.vertices = 0.05))
sample_perc10 <- map(2020:2025, ~ run_milo_sampling(data_5k_cells$graph, data_5k_cells$meta.df, data_5k_cells$model, data_5k_cells$X_pca, seed=.x, sample.vertices = 0.1))
sample_perc15 <- map(2020:2025, ~ run_milo_sampling(data_5k_cells$graph, data_5k_cells$meta.df, data_5k_cells$model, data_5k_cells$X_pca, seed=.x, sample.vertices = 0.15))
sample_perc20 <- map(2020:2025, ~ run_milo_sampling(data_5k_cells$graph, data_5k_cells$meta.df, data_5k_cells$model, data_5k_cells$X_pca, seed=.x, sample.vertices = 0.2))


make_test_df <- function(sample_df){
  sample_df %>%
  imap( ~ bind_rows(.x[["refined"]] %>% dplyr::mutate(sampling="refined"),
                     .x[["random"]] %>% dplyr::mutate(sampling="random")) %>%
           dplyr::mutate(s=.y)) %>%
    purrr::reduce(bind_rows) %>%
    left_join(data_5k_cells$meta.df) %>%
    dplyr::mutate(group_id = factor(group_id, levels=paste0('M', 1:num_milestones))) %>%
    group_by(sampling, s, group_id) %>%
    summarise(mean_logFC=mean(logFC)) 
  }

map(list(perc5=sample_perc5, perc10=sample_perc10, perc15=sample_perc15, perc20=sample_perc20), ~ make_test_df(.x)) %>%
  imap( ~ dplyr::mutate(.x, perc=.y)) %>%
  purrr::reduce(bind_rows) %>%
  ggplot(aes(group_id, mean_logFC, color=perc)) +
  # geom_pointrange(stat = "summary",
  #   fun.min = min,
  #   fun.max = max,
  #   fun = mean) +
  geom_boxplot(varwidth = TRUE) +
  facet_grid(.~sampling) +
  scale_fill_gradient2()

No big differences TBH


Compositional effect

Do I get high/low FC where unexpected just because things are changing elsewhere?

data_2k_cells <- simulate_linear_traj(num_cells = 2000, num_milestones = 10, prob_start = 0.5, prob_end=0.95)
ggplot(data_2k_cells$meta.df, aes(UMAP1, UMAP2, color=condition)) + geom_point(size=0.2) +
  theme_clean() 
ggplot(data_2k_cells$meta.df, aes(UMAP1, UMAP2, color=group_id)) + geom_point(size=0.2) +
  theme_clean() +
  geom_text(data = . %>% group_by(group_id) %>% summarise(UMAP1=first(UMAP1), UMAP2=first(UMAP2)), aes(label=group_id), color="black")

graph <- data_2k_cells$graph
sample.vertices <- 0.1
meta.df <- data_2k_cells$meta.df
model <- data_2k_cells$model
X_pca <- data_2k_cells$X_pca

random.vertices <- sample(V(graph), size=floor(sample.vertices*length(V(graph))))
vertex.knn <- BiocNeighbors::findKNN(X=X_pca, k=21, subset=as.vector(random.vertices))
refined.vertices <- V(graph)[sapply(1:nrow(vertex.knn$index), function(i) refine_vertex(vertex.knn, i, X_pca))]

vertex.list <- sapply(1:length(random.vertices), FUN=function(X) neighbors(graph, v=random.vertices[X]))
vertex.list.refined <- sapply(1:length(refined.vertices), FUN=function(X) neighbors(graph, v=refined.vertices[X]))

count.matrix.random <- countCells(graph, meta.df, vertex.list = vertex.list, random.vertices = random.vertices, sample.column = "sample")
count.matrix.refined <- countCells(graph, meta.df, vertex.list = vertex.list.refined, random.vertices = refined.vertices, sample.column = "sample")
  
spFDR.random <- testQLF(graph, count.matrix.random, model)
spFDR.refined <- testQLF(graph, count.matrix.refined, model)

Refined sampling seems to be able to identify DA at both ends of the spectrum better

fdr.df.random <- data.frame(Vertex=as.integer(rownames(spFDR.random$res)), p=spFDR.random$res$PValue, adjp=spFDR.random$spFDR, adjp_fdr=spFDR.random$res$FDR, logFC=spFDR.random$res$logFC, Sig=spFDR.random$res$Sig)
fdr.df.refined <- data.frame(Vertex=as.integer(rownames(spFDR.refined$res)), p=spFDR.refined$res$PValue, adjp=spFDR.refined$spFDR, logFC=spFDR.refined$res$logFC, adjp_fdr=spFDR.refined$res$FDR, Sig=spFDR.refined$res$Sig)

meta.df %>%
  left_join(fdr.df.random) %>%
  # dplyr::arrange(sampled) %>%
  ggplot(aes(UMAP1, UMAP2, 
             # color= - log10(adjp),
            # color= - log10(p),
             color = logFC
             )) +
  geom_point(size=0.5) +
  geom_point(data=. %>% dplyr::filter(!is.na(adjp))) +
  theme_clean() +
  scale_color_gradient2(midpoint = 0, high = "red", low="blue",na.value ="grey80") +
  ggtitle("Random sampling")


meta.df %>%
  left_join(fdr.df.refined) %>%
  # dplyr::arrange(sampled) %>%
  ggplot(aes(UMAP1, UMAP2, 
             # color= - log10(adjp),
            # color= - log10(p),
             color = logFC
             )) +
  geom_point(size=0.5) +
  geom_point(data=. %>% dplyr::filter(!is.na(adjp))) +
  theme_clean() +
  scale_color_gradient2(midpoint = 0, high = "red", low="blue",na.value ="grey80") +
  ggtitle("Refined sampling")
meta.df %>%
   left_join(fdr.df.refined) %>%
  dplyr::filter(!is.na(logFC)) %>%
  ggplot(aes(logFC, -log10(adjp), shape=Sig, color=group_id)) +
  geom_point() +
  ggtitle("refined sampling") +
meta.df %>%
   left_join(fdr.df.random) %>%
  dplyr::filter(!is.na(logFC)) %>%
  ggplot(aes(logFC, -log10(adjp), shape=Sig, color=group_id)) +
  geom_point() +
  ggtitle("random sampling") 
num_milestones=10

meta.df %>%
  ungroup() %>%
  left_join(fdr.df.refined) %>%
  dplyr::mutate(group_id = factor(group_id, levels=paste0('M', 1:num_milestones))) %>%
  dplyr::filter(!is.na(logFC)) %>%
  ggplot(aes(group_id, logFC,  shape=Sig, color=group_id, size= -log10(adjp))) +
  geom_jitter() +
  ggtitle("refined sampling") +
meta.df %>%
  ungroup() %>%
  left_join(fdr.df.random) %>%
  dplyr::mutate(group_id = factor(group_id, levels=paste0('M', 1:num_milestones))) %>%  dplyr::filter(!is.na(logFC)) %>%
  ggplot(aes(group_id, logFC, shape=Sig, color=group_id, size= -log10(adjp))) +
  geom_jitter() +
  ggtitle("random sampling") 

How many neighborhoods disappear w refinement?

simulate_linear_traj <- function(num_cells, num_milestones, num_features=1000, k_param=21, seed=42,
                                 prob_start=0.1, prob_end=0.9){
  set.seed(seed)
  ## Generate simulated dataset of trajectory
  dataset <- generate_dataset(
    model = model_linear(num_milestones = num_milestones),
    num_cells = num_cells,
    num_features = num_features
  )
  sim2.gex <- as.matrix(dataset$expression)
  sim2.branches <- dataset$prior_information$groups_id
  sim2.time = dataset$prior_information$timecourse_continuous
  
  ## Build graph 
  sim2.pca <- prcomp_irlba(sim2.gex, n=50, scale.=TRUE, center=TRUE)
  X_pca = sim2.pca$x[, c(1:30)]
  sim2.knn <- buildKNNGraph(x=X_pca, k=k_param, d=NA, transposed=TRUE)
  ## Run UMAP
  stem.ta.umap <- umap(sim2.pca$x[, c(1:30)],
                       n_components=2,
                       n_neighbors=k_param, metric='euclidean',
                       init='random', min_dist=0.1)
  dyn.df <- data.frame(UMAP1=stem.ta.umap$layout[,1], UMAP2=stem.ta.umap$layout[,2], 
             cell_id=rownames(sim2.gex), time=sim2.time)
  dyn.df <- dyn.df %>% left_join(sim2.branches)
  
  ## Simulate conditions
  n_groups <- length(unique(dyn.df$group_id))
  p_vec <- seq(prob_start, prob_end, length.out = n_groups)
  a.cells <- c()
  for (i in 1:n_groups) {
    g <- paste0("M",i)
    p <- p_vec[i] 
    m.A <- sample(dyn.df$cell_id[dyn.df$group_id==g], 
                  size=floor(sum(dyn.df$group_id==g)*p))
    a.cells <- c(a.cells, m.A)
  }
  
  dyn.df <- dyn.df %>% dplyr::mutate(condition = ifelse(cell_id %in% a.cells, "A", 'B')) 

  ## Simulate replicates
  dyn.df <- dyn.df %>%
    group_by(group_id) %>%
    dplyr::mutate(replicate=c(rep("R1", floor(n()*0.3)), 
                              rep("R2", floor(n()*0.3)), 
                              rep("R3", n() - 2*(floor(n()*0.3))))
    ) 
  
  ## Add sample name (condition + replicate)
  dyn.df$sample <- paste(dyn.df$condition, dyn.df$replicate, sep="_")
  ## Add vertex id (for counts)
  dyn.df$Vertex <- as.vector(V(sim2.knn))
  
  ## Make model matrix for testing
  sample.meta <- data.frame("Condition"=c(rep("A", 3), rep("B", 3)),
                            "Replicate"=rep(c("R1", "R2", "R3"), 2))
  sample.meta$Sample <- paste(sample.meta$Condition, sample.meta$Replicate, sep="_")
  rownames(sample.meta) <- sample.meta$Sample
  sim2.model <- model.matrix(~ 0 + Condition, data=sample.meta)
  
  return(list(graph=sim2.knn,
              X_pca=X_pca,
              meta.df=dyn.df,
              model=sim2.model))
  
  }
data_2k_cells <- simulate_linear_traj(num_cells = 2000, num_milestones = 10, prob_start = 0.5, prob_end=0.95)

graph <- data_2k_cells$graph
sample.vertices <- 0.1
meta.df <- data_2k_cells$meta.df
model <- data_2k_cells$model
X_pca <- data_2k_cells$X_pca

random.vertices <- sample(V(graph), size=floor(sample.vertices*length(V(graph))))
vertex.knn <- BiocNeighbors::findKNN(X=X_pca, k=21, subset=as.vector(random.vertices))
refined.vertices <- V(graph)[sapply(1:nrow(vertex.knn$index), function(i) refine_vertex(vertex.knn, i, X_pca))]


data.frame(random=as.numeric(random.vertices), refined=as.numeric(refined.vertices)) %>%
  rowid_to_column() %>%
  group_by(as.factor(refined)) %>%
  dplyr::mutate(n_converging = n()) %>%
  ungroup() %>%
  pivot_longer(cols=c('random', "refined"), names_to = "sampling_scheme", values_to = "Vertex") %>%
  left_join(meta.df, by="Vertex") %>%
  ggplot(aes(time,sampling_scheme, color=n_converging)) +
  geom_point(size=0.5) +
  geom_line(aes(group=rowid), size=0.5) +
  scale_color_viridis_c()
data.frame(random=as.numeric(random.vertices), refined=as.numeric(refined.vertices)) %>%
  rowid_to_column() %>%
  group_by(as.factor(refined)) %>%
  dplyr::mutate(n_converging = n()) %>%
  ggplot(aes(as.factor(n_converging))) + geom_histogram(stat="count")

After refinement what is the distance to the nearest sampled cell?

Distances should become more uniform

get_dist_to_closest_neigh <- function(graph, sample.vertices){
  random.vertices <- sample(V(graph), size=floor(sample.vertices*length(V(graph))))
  vertex.knn <- BiocNeighbors::findKNN(X=X_pca, k=21, subset=as.vector(random.vertices))
  refined.vertices <- V(graph)[sapply(1:nrow(vertex.knn$index), function(i) refine_vertex(vertex.knn, i, X_pca))]
  dist_to_closest_random <- BiocNeighbors::findKNN(X=X_pca[as.vector(random.vertices),], k=1)[["distance"]]
  dist_to_closest_refined <- BiocNeighbors::findKNN(X=X_pca[unique(as.vector(refined.vertices)),], k=1)[["distance"]]
  dist_df <- bind_rows(data.frame(distance_to_closest=dist_to_closest_refined, sampling_scheme='refined'),
          data.frame(distance_to_closest=dist_to_closest_random, sampling_scheme='random')) %>%
    dplyr::mutate(sample_perc=sample.vertices)
}

dist_ls <- map(seq(0.1,0.6, by = 0.05), ~ get_dist_to_closest_neigh(graph, .x)) 
purrr::reduce(dist_ls, bind_rows) %>%
  ggplot(aes(as.factor(sample_perc), distance_to_closest, color=sampling_scheme)) +
  # ggbeeswarm::geom_quasirandom()
  geom_boxplot(varwidth = TRUE) +
  xlab("% sampled") + ylab("Distance to closest sample") +
  theme_grey(base_size = 14)

What is the relationship between neighborhood size and k?

data_5k_cells <- simulate_linear_traj(num_cells = 5000, num_milestones = 10)
meta.df <- data_5k_cells$meta.df
model <- data_5k_cells$model
X_pca <- data_5k_cells$X_pca

k_vec <- seq(10,50, by=5)
graph_ls <- map(k_vec, ~ buildKNNGraph(x=X_pca, k=.x, d=NA, transposed=TRUE))
get_neigh_df <- function(sampled_vertices, graph, X_pca, k_param, sampling_mode="random"){
  if (sampling_mode=="refined") {
    vertex.knn <- BiocNeighbors::findKNN(X=X_pca, k=k_param, subset=as.vector(sampled_vertices))
    sampled_vertices <- V(graph)[sapply(1:nrow(vertex.knn$index), function(i) refine_vertex(vertex.knn, i, X_pca))]
  }
  sampled_vertices <- unique(sampled_vertices)
  vertex.list <- sapply(1:length(sampled_vertices), FUN=function(X) neighbors(graph, v=sampled_vertices[X]))
  neigh_df <- data.frame(neigh_vertex=as.vector(sampled_vertices), neigh_size=sapply(vertex.list, function(x) length(x)), 
                         sampling_mode=sampling_mode, k=k_param)
  return(neigh_df)
  }

neigh_df_ls <- lapply(seq_along(k_vec), function(i){
  random_sample <- sample(V(graph_ls[[i]]), size=floor(sample.vertices*length(V(graph_ls[[i]]))))
  sampled_vertices <- random_sample
  random_neigh_df <- get_neigh_df(random_sample, graph_ls[[i]], X_pca, k_vec[i], sampling_mode="random")
  refined_neigh_df <- get_neigh_df(random_sample, graph_ls[[i]], X_pca, k_vec[i], sampling_mode="refined")
  bind_rows(random_neigh_df, refined_neigh_df)
  })

purrr::reduce(neigh_df_ls, bind_rows) %>%
  ggplot(aes(as.factor(k), neigh_size, color=sampling_mode)) +
  geom_violin(scale = "width") +
  geom_boxplot(width=0.2) +
  facet_wrap(sampling_mode~.) +
  xlab("K") + ylab("Neighborhood size") +
  theme_clean(base_size = 18)
purrr::reduce(neigh_df_ls, bind_rows) %>%
  ggplot(aes(as.factor(k), neigh_size / k, color=sampling_mode)) +
  geom_violin(scale = "width") +
  geom_boxplot(width=0.2) +
  facet_wrap(sampling_mode~.) +
  xlab("K") + ylab("Neighborhood size / K") +
  theme_clean(base_size = 18)

Relationship between K and neighborhood size

purrr::reduce(neigh_df_ls, bind_rows) %>%
  group_by(sampling_mode, k) %>%
  summarise(n=n()) %>%
  ggplot(aes(k,n, color=sampling_mode)) +
  geom_point()
LS0tCnRpdGxlOiAiTWlsbyAtIHNpbXVsYXRlZCB0cmFqZWN0b3J5IGdyYXBocyIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9CmxpYnJhcnkoZ2dwbG90MikKbGlicmFyeShTaW5nbGVDZWxsRXhwZXJpbWVudCkKbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkoaWdyYXBoKQpsaWJyYXJ5KHNjcmFuKQoKZGV2dG9vbHM6OmxvYWRfYWxsKCJ+L21pbG8vbWlsb1IvIikKCiMgZGV2dG9vbHM6Omluc3RhbGxfZ2l0aHViKCJkeW52ZXJzZS9keW50b3kiKQpsaWJyYXJ5KGR5bnRveSkKYGBgCgojIyBUZXN0aW5nIHdpdGggcmFuZG9tIHNhbXBsaW5nCgojIyMgU2ltdWxhdGUgbGluZWFyIHRyYWplY3RvcnkKClVzaW5nIFtgZHludG95YF0oaHR0cHM6Ly9naXRodWIuY29tL2R5bnZlcnNlL2R5bnRveSksIEkgc2ltdWxhdGUgYSBzY1JOQS1zZXEgZGF0YSB3aXRoIGNlbGxzIGZvcm1pbmcgYSBsaW5lYXIgdHJhamVjdG9yeSAobm8gYnJhbmNoZXMpIHdpdGggNTAwMCBjZWxscyBhbmQgMTAgbWFpbiBzdGF0ZXMgKG1pbGVzdG9uZXMpLgoKYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9CnNldC5zZWVkKDQyKQpkYXRhc2V0IDwtIGdlbmVyYXRlX2RhdGFzZXQoCiAgbW9kZWwgPSBtb2RlbF9saW5lYXIobnVtX21pbGVzdG9uZXMgPSAxMCksCiAgbnVtX2NlbGxzID0gNTAwMCwKICBudW1fZmVhdHVyZXMgPSA1MDAwCikKCmdleCA8LSBhcy5tYXRyaXgoZGF0YXNldCRleHByZXNzaW9uKQpicmFuY2hlcyA8LSBkYXRhc2V0JHByaW9yX2luZm9ybWF0aW9uJGdyb3Vwc19pZAogIAojIyBEaW1lbnNpb25hbGl0eSByZWR1Y3Rpb24KcGNhIDwtIHByY29tcF9pcmxiYShnZXgsIG49NTAsIHNjYWxlLj1UUlVFLCBjZW50ZXI9VFJVRSkKWF9wY2EgPSBwY2EkeFssIGMoMTozMCldCmBgYAoKSSBhc3NpZ24gY2VsbHMgdG8gc2ltdWxhdGVkIGJpb2xvZ2ljYWwgY29uZGl0aW9ucyBhbmQgcmVwbGljYXRlcywgdGhhdCB3ZSB3aWxsIHVzZSBmb3IgZGlmZmVyZW50aWFsIGFidW5kYW5jZSB0ZXN0aW5nLiBGb3IgZWFjaCBvZiB0aGUgJE0kIGNsdXN0ZXJzLCBJIGFzc2lnbiBkaWZmZXJlbnQgcHJvcG9ydGlvbnMgb2YgY2VsbHMgdG8gY29uZGl0aW9uIEEgb3IgY29uZGl0aW9uIEIsIHdoaWxlIHNpbXVsYXRpbmcgcHJvcG9ydGlvbmF0ZSBtaXhpbmcgYmV0d2VlbiByZXBsaWNhdGVzLgoKYGBge3J9CmNvbGRhdGFfZGYgPC0gZGF0YS5mcmFtZShjZWxsX2lkID0gcm93bmFtZXMoZ2V4KSkKY29sZGF0YV9kZiA8LSBsZWZ0X2pvaW4oY29sZGF0YV9kZiwgYnJhbmNoZXMpCiAgCiMjIFNpbXVsYXRlIGJpb2xvZ2ljYWwgY29uZGl0aW9uCnByb2Jfc3RhcnQgPC0gMC4wNQpwcm9iX2VuZCA8LSAwLjk1Cm5fZ3JvdXBzIDwtIGxlbmd0aCh1bmlxdWUoYnJhbmNoZXMkZ3JvdXBfaWQpKQpwX3ZlYyA8LSBzZXEocHJvYl9zdGFydCwgcHJvYl9lbmQsIGxlbmd0aC5vdXQgPSBuX2dyb3VwcykKYS5jZWxscyA8LSBjKCkKZm9yIChpIGluIDE6bl9ncm91cHMpIHsKICBnIDwtIHBhc3RlMCgiTSIsaSkKICBwIDwtIHBfdmVjW2ldIAogIG0uQSA8LSBzYW1wbGUoY29sZGF0YV9kZiRjZWxsX2lkW2NvbGRhdGFfZGYkZ3JvdXBfaWQ9PWddLCAKICAgICAgICAgICAgICAgIHNpemU9Zmxvb3Ioc3VtKGNvbGRhdGFfZGYkZ3JvdXBfaWQ9PWcpKnApKQogIGEuY2VsbHMgPC0gYyhhLmNlbGxzLCBtLkEpCn0KCmNvbGRhdGFfZGYgPC0gY29sZGF0YV9kZiAlPiUgZHBseXI6Om11dGF0ZShjb25kaXRpb24gPSBpZmVsc2UoY2VsbF9pZCAlaW4lIGEuY2VsbHMsICJBIiwgJ0InKSkgCgojIyBTaW11bGF0ZSByZXBsaWNhdGVzCmNvbGRhdGFfZGYgPC0gY29sZGF0YV9kZiAlPiUKICBncm91cF9ieShncm91cF9pZCkgJT4lCiAgZHBseXI6Om11dGF0ZShyZXBsaWNhdGU9YyhyZXAoIlIxIiwgZmxvb3IobigpKjAuMykpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcCgiUjIiLCBmbG9vcihuKCkqMC4zKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwKCJSMyIsIG4oKSAtIDIqKGZsb29yKG4oKSowLjMpKSkpCiAgKSAKCiMjIEFkZCBzYW1wbGUgbmFtZSAoY29uZGl0aW9uICsgcmVwbGljYXRlKQpjb2xkYXRhX2RmJHNhbXBsZSA8LSBwYXN0ZShjb2xkYXRhX2RmJGNvbmRpdGlvbiwgY29sZGF0YV9kZiRyZXBsaWNhdGUsIHNlcD0iXyIpCgpoZWFkKGNvbGRhdGFfZGYpCmBgYAoKQ29uc3RydWN0IGEgYE1pbG9gIG9iamVjdAoKYGBge3J9CiMjIE1ha2UgU2luZ2xlQ2VsbEV4cGVyaW1lbnQgb2JqZWN0CmNvbGRhdGEgPC0gY29sZGF0YV9kZiAlPiUgY29sdW1uX3RvX3Jvd25hbWVzKCJjZWxsX2lkIikKc2ltX3NjZSA8LSBTaW5nbGVDZWxsRXhwZXJpbWVudChhc3NheT1saXN0KGNvdW50cz10KGdleCkpLCBjb2xEYXRhPWNvbGRhdGEpCmxvZ2NvdW50cyhzaW1fc2NlKSA8LSBsb2cyKGNvdW50cyhzaW1fc2NlKSArIDEpCnJlZHVjZWREaW1zKHNpbV9zY2UpIDwtIFNpbXBsZUxpc3QoUENBID0gWF9wY2EpCgojIyBNYWtlIG1pbG8gb2JqZWN0CnNpbV9taWxvIDwtIE1pbG8oc2ltX3NjZSkKc2ltX21pbG8KYGBgCgojIyMgQnVpbGQgS05OLWdyYXBoCgpgYGB7cn0Kc2ltX21pbG8gPC0gYnVpbGRHcmFwaChzaW1fbWlsbywgayA9IDIwLCBkID0gMzApCmBgYAoKIyMjIE1ha2UgbmVpZ2hib3VyaG9vZHMKClVzaW5nIHNhbXBsaW5nIG9mIDEwJSByYW5kb20gcG9pbnRzIAoKYGBge3J9CnNpbV9taWxvIDwtIG1ha2VOZWlnaGJvdXJob29kcyhzaW1fbWlsbywgcHJvcCA9IDAuMSwgaz0yMCwgZD0zMCwgcmVmaW5lZCA9IEZBTFNFKQpwbG90TmVpZ2hib3Job29kU2l6ZUhpc3Qoc2ltX21pbG8pCmBgYAoKIyMjIENvdW50IGNlbGxzIHdpdGhpbiBuZWlnaGJvdXJob29kcwpgYGB7cn0Kc2ltX21pbG8gPC0gY291bnRDZWxscyhzaW1fbWlsbywgZGF0YSA9IGRhdGEuZnJhbWUoY29sRGF0YShzaW1fbWlsbykpLCBzYW1wbGVzID0gInNhbXBsZSIpCmhlYWQobmVpZ2hib3VyaG9vZENvdW50cyhzaW1fbWlsbykpCmBgYAoKIyMjIFRlc3QgZm9yIERBCgpgYGB7cn0KIyMgQnVpbGQgZGVzaWduIG1hdHJpeApkZXNpZ25fZGYgPC0gZGF0YS5mcmFtZShjb2xEYXRhKHNpbV9taWxvKSkgJT4lCiAgcm93bmFtZXNfdG9fY29sdW1uKCkgJT4lCiAgc2VsZWN0KHNhbXBsZSwgY29uZGl0aW9uKSAlPiUKICBkaXN0aW5jdCgpIAoKbWlsb19yZXN1bHRzIDwtIHRlc3ROZWlnaGJvdXJob29kcyhzaW1fbWlsbywgfiAxICsgY29uZGl0aW9uLCBkYXRhID0gZGVzaWduX2RmLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZmRyLndlaWdodGluZyA9ICJrLWRpc3RhbmNlIikKCmhlYWQobWlsb19yZXN1bHRzKQpgYGAKCmBgYHtyfQptaWxvX3Jlc3VsdHMgJT4lCiAgbXV0YXRlKGlzX3NpZ25pZj1pZmVsc2UoU3BhdGlhbEZEUjwgMC4wNSwgMSwwKSkgJT4lCiAgZ2dwbG90KGFlcyhsb2dGQywgLWxvZzEwKFNwYXRpYWxGRFIpLCBjb2xvcj1pc19zaWduaWYpKSArIAogIGdlb21fcG9pbnQoKSArCiAgc2NhbGVfY29sb3JfZ3JhZGllbnQoaGlnaD0icmVkIikKYGBgCgpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZWNobz1UUlVFfQpuaEluZGV4IDwtIHVubGlzdChuZWlnaGJvdXJob29kSW5kZXgoc2ltX21pbG8pKQpuaFNpemUgPC0gc2FwcGx5KG5laWdoYm91cmhvb2RzKHNpbV9taWxvKSwgbGVuZ3RoKQptaWxvX3Jlc3VsdHMgPC0gbXV0YXRlKG1pbG9fcmVzdWx0cywgCiAgICAgICAgICAgICAgICAgICAgICAgbmhJbmRleD1uaEluZGV4LCBuaFNpemU9bmhTaXplKQoKZGYgPC0gY29sRGF0YShzaW1fbWlsbykgJT4lCiAgZGF0YS5mcmFtZSgpICU+JQogIHJvd2lkX3RvX2NvbHVtbigibmhJbmRleCIpICU+JQogIGxlZnRfam9pbihtaWxvX3Jlc3VsdHMpICU+JQogIG11dGF0ZShncm91cF9pZD1mYWN0b3IoZ3JvdXBfaWQsIGxldmVscz1zdHJfYygiTSIsIDE6MTApKSkKCmRmICU+JQogIGdncGxvdChhZXMoZ3JvdXBfaWQsIGxvZ0ZDKSkgKyAKICBnZW9tX2ppdHRlcihhZXMoY29sb3I9LWxvZzEwKFNwYXRpYWxGRFIpLCBzaXplPW5oU2l6ZSksIGFscGhhPTAuNikgKwogIHNjYWxlX2NvbG9yX3ZpcmlkaXNfYygpCmBgYAoKVmlzdWFsaXplIHJlc3VsdHMgaW4gZW1iZWRkaW5nCgpgYGB7cn0KIyMgQ29tcHV0ZSBVTUFQIGZvciBhbGwgY2VsbHMgCnNpbV91bWFwIDwtIHV3b3Q6OnVtYXAocmVkdWNlZERpbShzaW1fbWlsbyksIG5fbmVpZ2hib3JzID0gMjAsIG1ldHJpYyA9ICJjb3NpbmUiKQpyZWR1Y2VkRGltKHNpbV9taWxvLCAiVU1BUCIpIDwtIHNpbV91bWFwCgojIyBUYWtlIG1lZGlhbiBvZiBuZWlnaGJvdXJob29kcwpuaE1lZCA8LSB0KHNhcHBseShuZWlnaGJvdXJob29kcyhzaW1fbWlsbyksIGZ1bmN0aW9uKHgpIGNvbE1lZGlhbnMocmVkdWNlZERpbShzaW1fbWlsbylbeCxdKSkpCnVtYXBfbmhNZWQgPC0gdXdvdDo6dW1hcChuZWlnaGJvdXJob29kc01lZCwgbl9uZWlnaGJvcnM9MjAsIG1ldHJpYz0nY29zaW5lJywKICAgICAgICAgICBpbml0PXJlZHVjZWREaW0oc2ltX21pbG9bLHVubGlzdChuZWlnaGJvdXJob29kSW5kZXgoc2ltX21pbG8pKV0sICJVTUFQIiksIG1pbl9kaXN0PTAuMSkKY29sbmFtZXModW1hcF9uaE1lZCkgPC0gYygiVU1BUF8xIiwgIlVNQVBfMiIpCgptaWxvX3Jlc3VsdHNfdml6IDwtIGNiaW5kKG1pbG9fcmVzdWx0cywgdW1hcF9uaE1lZCkgCm1pbG9fcmVzdWx0c192aXogJT4lCiAgYXJyYW5nZShhYnMobG9nRkMpKSAlPiUKICBtdXRhdGUobG9nRkMgPSBpZmVsc2UoU3BhdGlhbEZEUiA+IGZpbHRlcl9hbHBoYSwgTkEsIGxvZ0ZDKSkgJT4lCiAgZ2dwbG90KGFlcyhVTUFQXzEsIFVNQVBfMikpICsKICBnZW9tX3BvaW50KGFlcyhjb2xvcj1sb2dGQykpICsKICBzY2FsZV9jb2xvcl9ncmFkaWVudDIobWlkcG9pbnQgPSAwLCBoaWdoPSJyZWQiLCBsb3c9ImJsdWUiLCBuYW1lPSJsb2ctRkMiKSArCiAgdGhlbWVfY2xhc3NpYyhiYXNlX3NpemUgPSAxNikgKwogIHRoZW1lKGF4aXMudGlja3MgPWVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0ID0gZWxlbWVudF9ibGFuaygpLCBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0PTAuNSkpCmBgYAoKYGBge3J9CnBsb3RNaWxvUmVkdWNlZERpbShzaW1fbWlsbywgbWlsb19yZXN1bHRzLCByZWR1Y2VkX2RpbSA9ICJVTUFQIiwgY29tcG9uZW50cyA9IGMoMSwyKSwgZmlsdGVyX2FscGhhID0gMC4xKQpgYGAKCiMjIFJlZmluZWQgc2FtcGxpbmcgc2NoZW1lCgpXZSBhZG9wdCB0aGUgcmVmaW5lZCBzYW1wbGluZyBzdHJhdGVneSBhcHBsaWVkIGluIFtXaXNoYm9uZV0oaHR0cHM6Ly93d3cubmF0dXJlLmNvbS9hcnRpY2xlcy9uYnQuMzU2OSNTZWMxMiksIGFuZCBhZGFwdGVkIGZyb20gW2hlcmVdKGh0dHBzOi8vd3d3Lm5hdHVyZS5jb20vYXJ0aWNsZXMvbm1ldGguMzU0NSkuIEJyaWVmbHksIHRvIGF2b2lkIHNlbGVjdGluZyBvdXRsaWVycyB3aXRoIHJhbmRvbSBzYW1wbGluZywgSSBmaXJzdCByYW5kb21seSBzZWxlY3QgJG4kIGNlbGxzLiBGb3IgZWFjaCBzYW1wbGVkIGNlbGwgSSB0aGVuIGlkZW50aWZ5IGl0cyBrIG5lYXJlcyBuZWlnaGJvcnMgYW5kIGNvbXB1dGUgdGhlIG1lZGlhbiBwcm9maWxlIG9mIHRoZSBuZWlnaGJvcnMgKGluIHRoaXMgY2FzZSB0aGUgcHJvZmlsZSBpbiByZWR1Y2VkIFBDIHNwYWNlKS4gVGhlbiBJIHJlcGxhY2UgZWFjaCBzYW1wbGVkIGNlbGwgYnkgdGhlIGNlbGwgY2xvc2VzdCB0byB0aGUgbWVkaWFuIHByb2ZpbGUgb2YgaXRzIG5laWdoYm9ycy4gCgpgYGB7cn0Kc2ltX21pbG9fcmVmIDwtIG1ha2VOZWlnaGJvdXJob29kcyhzaW1fbWlsbyxwcm9wID0gMC4xLCBrPTIwLCBkPTMwLCByZWZpbmVkID0gVFJVRSkKYGBgCgojIyMgUmVmaW5lZCBzYW1wbGluZyBzdGF0cwoKV2l0aCB0aGUgcmVmaW5lZCBzYW1wbGluZyBzY2hlbWUgSSBzZWxlY3QgY2VsbHMgd2l0aCBhIGxhcmdlciBuZWlnaGJvdXJob29kIG9uIGF2ZXJhZ2UuCgpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZWNobz1UUlVFfQpiaW5kX3Jvd3MoCiAgZGF0YS5mcmFtZShuZWlnaGJvcmhvb2Rfc2l6ZSA9IHVubGlzdChsYXBwbHkobmVpZ2hib3VyaG9vZHMoc2ltX21pbG8pLCBsZW5ndGgpKSwgc2FtcGxpbmc9InJhbmRvbSIpLAogIGRhdGEuZnJhbWUobmVpZ2hib3Job29kX3NpemUgPSB1bmxpc3QobGFwcGx5KG5laWdoYm91cmhvb2RzKHNpbV9taWxvX3JlZiksIGxlbmd0aCkpLCBzYW1wbGluZz0icmVmaW5lZCIpCiAgKSAlPiUKICBnZ3Bsb3QoYWVzKG5laWdoYm9yaG9vZF9zaXplLCBmaWxsPXNhbXBsaW5nKSkgKwogIGdlb21faGlzdG9ncmFtKGJpbnM9MzApICsKICBmYWNldF9ncmlkKHNhbXBsaW5nfi4sIHNjYWxlcz0iZnJlZV95IikgKwogIHRoZW1lX2dyZXkoYmFzZV9zaXplPTE1KQpgYGAKCldoZW4gJG4kIGlzIGxhcmdlIEkgb2Z0ZW4gZW5kIHVwIHNhbXBsaW5nIGxlc3MgdGhhbiAkbiQgY2VsbHMgYmVjYXVzZSBmb3IgbWFueSByYW5kb21seSBzYW1wbGVkIGNlbGxzIHRoZSBjZWxsIGNsb3Nlc3QgdG8gdGhlIEtOTnMgaXMgdGhlIHNhbWUuIAoKYGBge3J9CnJhbmRvbV9uIDwtIGMoKQpyZWZpbmVkX24gPC0gYygpCmZvciAoeCBpbiBzZXEoMC4wMSwwLjUsIGJ5ID0gMC4wNSkpIHsKICBmb3IgKGkgaW4gMTozKXsKICAgIHJhbmRvbV9uaCAgPC0gbmVpZ2hib3VyaG9vZHMobWFrZU5laWdoYm91cmhvb2RzKHNpbV9taWxvLCBwcm9wPXgsIGs9MjAsIGQ9MzAsIHJlZmluZWQgPSBGQUxTRSwgc2VlZD00MiArIGkpKQogICAgcmVmaW5lZF9uaCA8LSBuZWlnaGJvdXJob29kcyhtYWtlTmVpZ2hib3VyaG9vZHMoc2ltX21pbG8sIHByb3A9eCwgaz0yMCwgZD0zMCwgcmVmaW5lZCA9IFRSVUUsIHNlZWQ9NDIgKyBpKSkKICAgIGxfcmFuIDwtIGxlbmd0aChyYW5kb21fbmgpCiAgICBsX3JlZiA8LSBsZW5ndGgocmVmaW5lZF9uaCkKICAgIHJhbmRvbV9uIDwtIGMocmFuZG9tX24sIGxfcmFuKQogICAgcmVmaW5lZF9uIDwtIGMocmVmaW5lZF9uLCBsX3JlZikKICAgIH0KfQoKZGF0YS5mcmFtZShzYW1wbGVfcHJvcG9ydGlvbj1sYXBwbHkoc2VxKDAuMDEsMC41LCBieSA9IDAuMDUpLCBmdW5jdGlvbih4KSByZXAoeCwgMykpICU+JSBwdXJycjo6cmVkdWNlKGMpLCByYW5kb20gPSByYW5kb21fbiwgcmVmaW5lZCA9IHJlZmluZWRfbikgJT4lCiAgcGl2b3RfbG9uZ2VyKGNvbHMgPSAtIHNhbXBsZV9wcm9wb3J0aW9uLCBuYW1lc190byA9ICdzYW1wbGluZycsIHZhbHVlc190byA9ICJuIikgJT4lCiAgZ2dwbG90KGFlcyhzYW1wbGVfcHJvcG9ydGlvbiwgbiwgY29sb3I9c2FtcGxpbmcpKSArIAogIGdlb21fcG9pbnQoc2l6ZT0xKSArCiAgdGhlbWVfYncoYmFzZV9zaXplID0gMTYpCmBgYAoKIyMjIENvbXBhcmUgREEgdGVzdGluZyByZXN1bHRzCgpgYGB7cn0Kc2ltX21pbG9fcmVmIDwtIGNvdW50Q2VsbHMoc2ltX21pbG9fcmVmLCBkYXRhID0gZGF0YS5mcmFtZShjb2xEYXRhKHNpbV9taWxvX3JlZikpLCBzYW1wbGVzID0gInNhbXBsZSIpCgpyZXNfcmFuZCA8LSB0ZXN0TmVpZ2hib3VyaG9vZHMoc2ltX21pbG8sIH4gMSArIGNvbmRpdGlvbiwgZGF0YSA9IGRlc2lnbl9kZiwgZmRyLndlaWdodGluZyA9ICJrLWRpc3RhbmNlIikKcmVzX3JlZiA8LSB0ZXN0TmVpZ2hib3VyaG9vZHMoc2ltX21pbG9fcmVmLCB+IDEgKyBjb25kaXRpb24sIGRhdGEgPSBkZXNpZ25fZGYsIGZkci53ZWlnaHRpbmcgPSAiay1kaXN0YW5jZSIpCmBgYAoKUmVmaW5lZCBzYW1wbGluZyBzZWVtcyB0byBiZSBhYmxlIHRvIGlkZW50aWZ5IERBIGF0IGJvdGggZW5kcyBvZiB0aGUgc3BlY3RydW0gYmV0dGVyCgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KcGxvdE1pbG9SZWR1Y2VkRGltKHNpbV9taWxvLCByZXNfcmFuZCwgcHRfc2l6ZT0yKSArIGdndGl0bGUoIlJhbmRvbSBzYW1wbGluZyIpIApwbG90TWlsb1JlZHVjZWREaW0oc2ltX21pbG9fcmVmLCByZXNfcmVmLCBwdF9zaXplPTIpICsgZ2d0aXRsZSgiUmVmaW5lZCBzYW1wbGluZyIpCmBgYAoKPCEtLSBgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRSwgZmlnLndpZHRoPTEwLCBmaWcuaGVpZ2h0PTV9IC0tPgo8IS0tIGR5bi5kZiAlPiUgLS0+CjwhLS0gICAgbGVmdF9qb2luKGZkci5kZi5yZWZpbmVkKSAlPiUgLS0+CjwhLS0gICBkcGx5cjo6ZmlsdGVyKCFpcy5uYShsb2dGQykpICU+JSAtLT4KPCEtLSAgIGdncGxvdChhZXMobG9nRkMsIC1sb2cxMChhZGpwKSwgc2hhcGU9U2lnLCBjb2xvcj1ncm91cF9pZCkpICsgLS0+CjwhLS0gICBnZW9tX3BvaW50KCkgKyAtLT4KPCEtLSAgIGdndGl0bGUoInJlZmluZWQgc2FtcGxpbmciKSArIC0tPgo8IS0tIGR5bi5kZiAlPiUgLS0+CjwhLS0gICAgbGVmdF9qb2luKGZkci5kZi5yYW5kb20pICU+JSAtLT4KPCEtLSAgIGRwbHlyOjpmaWx0ZXIoIWlzLm5hKGxvZ0ZDKSkgJT4lIC0tPgo8IS0tICAgZ2dwbG90KGFlcyhsb2dGQywgLWxvZzEwKGFkanApLCBzaGFwZT1TaWcsIGNvbG9yPWdyb3VwX2lkKSkgKyAtLT4KPCEtLSAgIGdlb21fcG9pbnQoKSArIC0tPgo8IS0tICAgZ2d0aXRsZSgicmFuZG9tIHNhbXBsaW5nIikgIC0tPgo8IS0tIGBgYCAtLT4KCgo8IS0tIGBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBmaWcud2lkdGg9MTAsIGZpZy5oZWlnaHQ9NX0gLS0+CjwhLS0gZHluLmRmICU+JSAtLT4KPCEtLSAgICBsZWZ0X2pvaW4oZmRyLmRmLnJlZmluZWQpICU+JSAtLT4KPCEtLSAgIGRwbHlyOjpmaWx0ZXIoIWlzLm5hKGxvZ0ZDKSkgJT4lIC0tPgo8IS0tICAgZ2dwbG90KGFlcyhncm91cF9pZCwgbG9nRkMsICBzaGFwZT1TaWcsIGNvbG9yPWdyb3VwX2lkLCBzaXplPSAtbG9nMTAoYWRqcCkpKSArIC0tPgo8IS0tICAgZ2VvbV9qaXR0ZXIoKSArIC0tPgo8IS0tICAgZ2d0aXRsZSgicmVmaW5lZCBzYW1wbGluZyIpICsgLS0+CjwhLS0gZHluLmRmICU+JSAtLT4KPCEtLSAgICBsZWZ0X2pvaW4oZmRyLmRmLnJhbmRvbSkgJT4lIC0tPgo8IS0tICAgZHBseXI6OmZpbHRlcighaXMubmEobG9nRkMpKSAlPiUgLS0+CjwhLS0gICBnZ3Bsb3QoYWVzKGdyb3VwX2lkLCBsb2dGQywgc2hhcGU9U2lnLCBjb2xvcj1ncm91cF9pZCwgc2l6ZT0gLWxvZzEwKGFkanApKSkgKyAtLT4KPCEtLSAgIGdlb21faml0dGVyKCkgKyAtLT4KPCEtLSAgIGdndGl0bGUoInJhbmRvbSBzYW1wbGluZyIpICAtLT4KPCEtLSBgYGAgLS0+CgpBcyBleHBlY3RlZCBtdWx0aXBsZSB0ZXN0aW5nIGNvcnJlY3Rpb24gaXMgbGVzcyBzZXZlcmUgd2l0aCB0aGUgcmVmaW5lZCBzYW1wbGUgc2V0IChsZXNzIHBvaW50cykKCmBgYHtyfQpyZXNfcmFuZCAlPiUKICBnZ3Bsb3QoYWVzKC1sb2cxMChQVmFsdWUpLCAtbG9nMTAoU3BhdGlhbEZEUikpKSArCiAgZ2VvbV9hYmxpbmUobGluZXR5cGU9MikgKwogIGdlb21fcG9pbnQoKSArCiAgZ2d0aXRsZSgiUmVmaW5lZCBzYW1wbGluZyIpIApyZXNfcmVmICU+JQogIGdncGxvdChhZXMoLWxvZzEwKFBWYWx1ZSksIC1sb2cxMChTcGF0aWFsRkRSKSkpICsKICBnZW9tX2FibGluZShsaW5ldHlwZT0yKSArCiAgZ2d0aXRsZSgiUmFuZG9tIHNhbXBsaW5nIikgKwogIGdlb21fcG9pbnQoKSAKCmBgYAoKIyMgUGlja2luZyBzYW1wbGluZyBwcm9wb3J0aW9uIGFuZCBrCgpJIHdhbnQgdG8gc2VsZWN0IHRoZXNlIHBhcmFtZXRlcnMgdG8gaW5jcmVhc2UgbWVhbiBuaCBzaXplCgpgYGB7ciwgZmlnLmhlaWdodD0xMCwgZmlnLndpZHRoPTEwfQp0ZXN0X21lYW5fbmhfc2l6ZSA8LSBmdW5jdGlvbihtLCBwcm9wLCBrLCBuX2NlbGxzKXsKICBtIDwtIG1bLHNhbXBsZShjb2xuYW1lcyhtKSwgc2l6ZSA9IG5fY2VsbHMpXQogIG0gPC0gYnVpbGRHcmFwaChtLCBrID0gaywgZCA9IDMwKQogIHJlZmluZWRfbmggPC0gbmVpZ2hib3VyaG9vZHMobWFrZU5laWdoYm91cmhvb2RzKG0sIHByb3A9cHJvcCwgaz1rLCBkPTMwLCByZWZpbmVkID0gVFJVRSwgc2VlZD00MikpCiAgcmV0dXJuKG1lYW4oc2FwcGx5KHJlZmluZWRfbmgsIGxlbmd0aCkpKQp9Cgpwcm9wX3ZlYyA8LSBzZXEoMC4wNSwwLjI1LCBieSA9IDAuMDUpCmtfdmVjIDwtIHNlcSgxMCw1MCwgYnk9MTApCm5jZWxsc192ZWMgPC0gc2VxKDEwMDAsIDUwMDAsIGJ5PTEwMDApCgpncmlkX2RmIDwtIGV4cGFuZC5ncmlkKG5jZWxsc192ZWMsIGtfdmVjLCBwcm9wX3ZlYykKY29sbmFtZXMoZ3JpZF9kZikgPC0gYygibl9jZWxscyIsICJrIiwgInByb3AiKQoKbWVhbl9uaF9zaXplcyA8LSBhcHBseShncmlkX2RmLCAxLCBmdW5jdGlvbih4KSB0ZXN0X21lYW5fbmhfc2l6ZShtLCB4WyJwcm9wIl0sIHhbImsiXSwgeFsibl9jZWxscyJdKSkKCgpncmlkX2RmICU+JQogIG11dGF0ZShtZWFuX25oX3NpemU9bWVhbl9uaF9zaXplcykgJT4lCiAgZ2dwbG90KGFlcyhwcm9wLCBtZWFuX25oX3NpemUpKSArCiAgZ2VvbV9wb2ludCgpICsKICAjIGdlb21fdGlsZShhZXMoZmlsbD1tZWFuX25oX3NpemUpKSArCiAgIyBzY2FsZV9maWxsX3ZpcmlkaXNfYygpICsKICBmYWNldF9ncmlkKG5fY2VsbHN+aykKCmBgYAoKCgotLS0KCiMjIE9sZCBjb2RlCgojIyBSb2J1c3RuZXNzIG9mIHRlc3Qgb3V0Y29tZXMKCkkgd2FudCB0byBjaGVjayB3aGV0aGVyIHVzaW5nIHJlZmluZWQgc2FtcGxpbmcgYWxsb3dzIHRvIGhhdmUgbW9yZSBsb2dGQyBldmVuIHdpdGggZGlmZmVyZW50IHNhbXBsaW5nIAoKYGBge3J9CnNwRkRSLnJhbmRvbSRyZXMKaW50ZXJzZWN0KHJhbmRvbS52ZXJ0aWNlcywgcmVmaW5lZC52ZXJ0aWNlcykKYGBgCgoKYGBge3IsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD03fQpydW5fbWlsb19zYW1wbGluZyA8LSBmdW5jdGlvbihncmFwaCwgbWV0YS5kZiwgbW9kZWwsIFhfcGNhLCBzZWVkPTQyLCBzYW1wbGUudmVydGljZXM9MC4xKXsKICBzZXQuc2VlZChzZWVkKQogIHJhbmRvbS52ZXJ0aWNlcyA8LSBzYW1wbGUoVihncmFwaCksIHNpemU9Zmxvb3Ioc2FtcGxlLnZlcnRpY2VzKmxlbmd0aChWKGdyYXBoKSkpKQogIHZlcnRleC5rbm4gPC0gQmlvY05laWdoYm9yczo6ZmluZEtOTihYPVhfcGNhLCBrPTIxLCBzdWJzZXQ9YXMudmVjdG9yKHJhbmRvbS52ZXJ0aWNlcykpCiAgcmVmaW5lZC52ZXJ0aWNlcyA8LSBWKGdyYXBoKVtzYXBwbHkoMTpucm93KHZlcnRleC5rbm4kaW5kZXgpLCBmdW5jdGlvbihpKSByZWZpbmVfdmVydGV4KHZlcnRleC5rbm4sIGksIFhfcGNhKSldCiAgCiAgdmVydGV4Lmxpc3QgPC0gc2FwcGx5KDE6bGVuZ3RoKHJhbmRvbS52ZXJ0aWNlcyksIEZVTj1mdW5jdGlvbihYKSBuZWlnaGJvcnMoZ3JhcGgsIHY9cmFuZG9tLnZlcnRpY2VzW1hdKSkKICB2ZXJ0ZXgubGlzdC5yZWZpbmVkIDwtIHNhcHBseSgxOmxlbmd0aChyZWZpbmVkLnZlcnRpY2VzKSwgRlVOPWZ1bmN0aW9uKFgpIG5laWdoYm9ycyhncmFwaCwgdj1yZWZpbmVkLnZlcnRpY2VzW1hdKSkKICAKICBjb3VudC5tYXRyaXgucmFuZG9tIDwtIGNvdW50Q2VsbHMoc2ltMi5rbm4sIG1ldGEuZGYsIHZlcnRleC5saXN0ID0gdmVydGV4Lmxpc3QsIHJhbmRvbS52ZXJ0aWNlcyA9IHJhbmRvbS52ZXJ0aWNlcywgc2FtcGxlLmNvbHVtbiA9ICJzYW1wbGUiKQogIGNvdW50Lm1hdHJpeC5yZWZpbmVkIDwtIGNvdW50Q2VsbHMoc2ltMi5rbm4sIG1ldGEuZGYsIHZlcnRleC5saXN0ID0gdmVydGV4Lmxpc3QucmVmaW5lZCwgcmFuZG9tLnZlcnRpY2VzID0gcmVmaW5lZC52ZXJ0aWNlcywgc2FtcGxlLmNvbHVtbiA9ICJzYW1wbGUiKQogICAgCiAgc3BGRFIucmFuZG9tIDwtIHRlc3RRTEYoZ3JhcGgsIGNvdW50Lm1hdHJpeC5yYW5kb20sIG1vZGVsKQogIHNwRkRSLnJlZmluZWQgPC0gdGVzdFFMRihncmFwaCwgY291bnQubWF0cml4LnJlZmluZWQsIG1vZGVsKQogIAogIGZkci5kZi5yYW5kb20gPC0gZGF0YS5mcmFtZShWZXJ0ZXg9YXMuaW50ZWdlcihyb3duYW1lcyhzcEZEUi5yYW5kb20kcmVzKSksIHA9c3BGRFIucmFuZG9tJHJlcyRQVmFsdWUsIGFkanA9c3BGRFIucmFuZG9tJHNwRkRSLCBhZGpwX2Zkcj1zcEZEUi5yYW5kb20kcmVzJEZEUiwgbG9nRkM9c3BGRFIucmFuZG9tJHJlcyRsb2dGQywgU2lnPXNwRkRSLnJhbmRvbSRyZXMkU2lnKQogIGZkci5kZi5yZWZpbmVkIDwtIGRhdGEuZnJhbWUoVmVydGV4PWFzLmludGVnZXIocm93bmFtZXMoc3BGRFIucmVmaW5lZCRyZXMpKSwgcD1zcEZEUi5yZWZpbmVkJHJlcyRQVmFsdWUsIGFkanA9c3BGRFIucmVmaW5lZCRzcEZEUiwgbG9nRkM9c3BGRFIucmVmaW5lZCRyZXMkbG9nRkMsIGFkanBfZmRyPXNwRkRSLnJlZmluZWQkcmVzJEZEUiwgU2lnPXNwRkRSLnJlZmluZWQkcmVzJFNpZykKCiAgcmV0dXJuKGxpc3QocmFuZG9tPWZkci5kZi5yYW5kb20sIHJlZmluZWQ9ZmRyLmRmLnJlZmluZWQpKQp9CgpzYW1wbGVfcGVyYzUgPC0gbWFwKDIwMjA6MjAyNSwgfiBydW5fbWlsb19zYW1wbGluZyhkYXRhXzVrX2NlbGxzJGdyYXBoLCBkYXRhXzVrX2NlbGxzJG1ldGEuZGYsIGRhdGFfNWtfY2VsbHMkbW9kZWwsIGRhdGFfNWtfY2VsbHMkWF9wY2EsIHNlZWQ9LngsIHNhbXBsZS52ZXJ0aWNlcyA9IDAuMDUpKQpzYW1wbGVfcGVyYzEwIDwtIG1hcCgyMDIwOjIwMjUsIH4gcnVuX21pbG9fc2FtcGxpbmcoZGF0YV81a19jZWxscyRncmFwaCwgZGF0YV81a19jZWxscyRtZXRhLmRmLCBkYXRhXzVrX2NlbGxzJG1vZGVsLCBkYXRhXzVrX2NlbGxzJFhfcGNhLCBzZWVkPS54LCBzYW1wbGUudmVydGljZXMgPSAwLjEpKQpzYW1wbGVfcGVyYzE1IDwtIG1hcCgyMDIwOjIwMjUsIH4gcnVuX21pbG9fc2FtcGxpbmcoZGF0YV81a19jZWxscyRncmFwaCwgZGF0YV81a19jZWxscyRtZXRhLmRmLCBkYXRhXzVrX2NlbGxzJG1vZGVsLCBkYXRhXzVrX2NlbGxzJFhfcGNhLCBzZWVkPS54LCBzYW1wbGUudmVydGljZXMgPSAwLjE1KSkKc2FtcGxlX3BlcmMyMCA8LSBtYXAoMjAyMDoyMDI1LCB+IHJ1bl9taWxvX3NhbXBsaW5nKGRhdGFfNWtfY2VsbHMkZ3JhcGgsIGRhdGFfNWtfY2VsbHMkbWV0YS5kZiwgZGF0YV81a19jZWxscyRtb2RlbCwgZGF0YV81a19jZWxscyRYX3BjYSwgc2VlZD0ueCwgc2FtcGxlLnZlcnRpY2VzID0gMC4yKSkKCgptYWtlX3Rlc3RfZGYgPC0gZnVuY3Rpb24oc2FtcGxlX2RmKXsKICBzYW1wbGVfZGYgJT4lCiAgaW1hcCggfiBiaW5kX3Jvd3MoLnhbWyJyZWZpbmVkIl1dICU+JSBkcGx5cjo6bXV0YXRlKHNhbXBsaW5nPSJyZWZpbmVkIiksCiAgICAgICAgICAgICAgICAgICAgIC54W1sicmFuZG9tIl1dICU+JSBkcGx5cjo6bXV0YXRlKHNhbXBsaW5nPSJyYW5kb20iKSkgJT4lCiAgICAgICAgICAgZHBseXI6Om11dGF0ZShzPS55KSkgJT4lCiAgICBwdXJycjo6cmVkdWNlKGJpbmRfcm93cykgJT4lCiAgICBsZWZ0X2pvaW4oZGF0YV81a19jZWxscyRtZXRhLmRmKSAlPiUKICAgIGRwbHlyOjptdXRhdGUoZ3JvdXBfaWQgPSBmYWN0b3IoZ3JvdXBfaWQsIGxldmVscz1wYXN0ZTAoJ00nLCAxOm51bV9taWxlc3RvbmVzKSkpICU+JQogICAgZ3JvdXBfYnkoc2FtcGxpbmcsIHMsIGdyb3VwX2lkKSAlPiUKICAgIHN1bW1hcmlzZShtZWFuX2xvZ0ZDPW1lYW4obG9nRkMpKSAKICB9CgptYXAobGlzdChwZXJjNT1zYW1wbGVfcGVyYzUsIHBlcmMxMD1zYW1wbGVfcGVyYzEwLCBwZXJjMTU9c2FtcGxlX3BlcmMxNSwgcGVyYzIwPXNhbXBsZV9wZXJjMjApLCB+IG1ha2VfdGVzdF9kZigueCkpICU+JQogIGltYXAoIH4gZHBseXI6Om11dGF0ZSgueCwgcGVyYz0ueSkpICU+JQogIHB1cnJyOjpyZWR1Y2UoYmluZF9yb3dzKSAlPiUKICBnZ3Bsb3QoYWVzKGdyb3VwX2lkLCBtZWFuX2xvZ0ZDLCBjb2xvcj1wZXJjKSkgKwogICMgZ2VvbV9wb2ludHJhbmdlKHN0YXQgPSAic3VtbWFyeSIsCiAgIyAgIGZ1bi5taW4gPSBtaW4sCiAgIyAgIGZ1bi5tYXggPSBtYXgsCiAgIyAgIGZ1biA9IG1lYW4pICsKICBnZW9tX2JveHBsb3QodmFyd2lkdGggPSBUUlVFKSArCiAgZmFjZXRfZ3JpZCgufnNhbXBsaW5nKSArCiAgc2NhbGVfZmlsbF9ncmFkaWVudDIoKQpgYGAKCk5vIGJpZyBkaWZmZXJlbmNlcyBUQkgKCi0tLQoKIyMgQ29tcG9zaXRpb25hbCBlZmZlY3QKCkRvIEkgZ2V0IGhpZ2gvbG93IEZDIHdoZXJlIHVuZXhwZWN0ZWQganVzdCBiZWNhdXNlIHRoaW5ncyBhcmUgY2hhbmdpbmcgZWxzZXdoZXJlPwoKCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQpkYXRhXzJrX2NlbGxzIDwtIHNpbXVsYXRlX2xpbmVhcl90cmFqKG51bV9jZWxscyA9IDIwMDAsIG51bV9taWxlc3RvbmVzID0gMTAsIHByb2Jfc3RhcnQgPSAwLjUsIHByb2JfZW5kPTAuOTUpCmBgYAoKYGBge3J9CmdncGxvdChkYXRhXzJrX2NlbGxzJG1ldGEuZGYsIGFlcyhVTUFQMSwgVU1BUDIsIGNvbG9yPWNvbmRpdGlvbikpICsgZ2VvbV9wb2ludChzaXplPTAuMikgKwogIHRoZW1lX2NsZWFuKCkgCmdncGxvdChkYXRhXzJrX2NlbGxzJG1ldGEuZGYsIGFlcyhVTUFQMSwgVU1BUDIsIGNvbG9yPWdyb3VwX2lkKSkgKyBnZW9tX3BvaW50KHNpemU9MC4yKSArCiAgdGhlbWVfY2xlYW4oKSArCiAgZ2VvbV90ZXh0KGRhdGEgPSAuICU+JSBncm91cF9ieShncm91cF9pZCkgJT4lIHN1bW1hcmlzZShVTUFQMT1maXJzdChVTUFQMSksIFVNQVAyPWZpcnN0KFVNQVAyKSksIGFlcyhsYWJlbD1ncm91cF9pZCksIGNvbG9yPSJibGFjayIpCmBgYAoKYGBge3J9CgpncmFwaCA8LSBkYXRhXzJrX2NlbGxzJGdyYXBoCnNhbXBsZS52ZXJ0aWNlcyA8LSAwLjEKbWV0YS5kZiA8LSBkYXRhXzJrX2NlbGxzJG1ldGEuZGYKbW9kZWwgPC0gZGF0YV8ya19jZWxscyRtb2RlbApYX3BjYSA8LSBkYXRhXzJrX2NlbGxzJFhfcGNhCgpyYW5kb20udmVydGljZXMgPC0gc2FtcGxlKFYoZ3JhcGgpLCBzaXplPWZsb29yKHNhbXBsZS52ZXJ0aWNlcypsZW5ndGgoVihncmFwaCkpKSkKdmVydGV4LmtubiA8LSBCaW9jTmVpZ2hib3JzOjpmaW5kS05OKFg9WF9wY2EsIGs9MjEsIHN1YnNldD1hcy52ZWN0b3IocmFuZG9tLnZlcnRpY2VzKSkKcmVmaW5lZC52ZXJ0aWNlcyA8LSBWKGdyYXBoKVtzYXBwbHkoMTpucm93KHZlcnRleC5rbm4kaW5kZXgpLCBmdW5jdGlvbihpKSByZWZpbmVfdmVydGV4KHZlcnRleC5rbm4sIGksIFhfcGNhKSldCgp2ZXJ0ZXgubGlzdCA8LSBzYXBwbHkoMTpsZW5ndGgocmFuZG9tLnZlcnRpY2VzKSwgRlVOPWZ1bmN0aW9uKFgpIG5laWdoYm9ycyhncmFwaCwgdj1yYW5kb20udmVydGljZXNbWF0pKQp2ZXJ0ZXgubGlzdC5yZWZpbmVkIDwtIHNhcHBseSgxOmxlbmd0aChyZWZpbmVkLnZlcnRpY2VzKSwgRlVOPWZ1bmN0aW9uKFgpIG5laWdoYm9ycyhncmFwaCwgdj1yZWZpbmVkLnZlcnRpY2VzW1hdKSkKCmNvdW50Lm1hdHJpeC5yYW5kb20gPC0gY291bnRDZWxscyhncmFwaCwgbWV0YS5kZiwgdmVydGV4Lmxpc3QgPSB2ZXJ0ZXgubGlzdCwgcmFuZG9tLnZlcnRpY2VzID0gcmFuZG9tLnZlcnRpY2VzLCBzYW1wbGUuY29sdW1uID0gInNhbXBsZSIpCmNvdW50Lm1hdHJpeC5yZWZpbmVkIDwtIGNvdW50Q2VsbHMoZ3JhcGgsIG1ldGEuZGYsIHZlcnRleC5saXN0ID0gdmVydGV4Lmxpc3QucmVmaW5lZCwgcmFuZG9tLnZlcnRpY2VzID0gcmVmaW5lZC52ZXJ0aWNlcywgc2FtcGxlLmNvbHVtbiA9ICJzYW1wbGUiKQogIApzcEZEUi5yYW5kb20gPC0gdGVzdFFMRihncmFwaCwgY291bnQubWF0cml4LnJhbmRvbSwgbW9kZWwpCnNwRkRSLnJlZmluZWQgPC0gdGVzdFFMRihncmFwaCwgY291bnQubWF0cml4LnJlZmluZWQsIG1vZGVsKQoKYGBgCgpSZWZpbmVkIHNhbXBsaW5nIHNlZW1zIHRvIGJlIGFibGUgdG8gaWRlbnRpZnkgREEgYXQgYm90aCBlbmRzIG9mIHRoZSBzcGVjdHJ1bSBiZXR0ZXIKCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpmZHIuZGYucmFuZG9tIDwtIGRhdGEuZnJhbWUoVmVydGV4PWFzLmludGVnZXIocm93bmFtZXMoc3BGRFIucmFuZG9tJHJlcykpLCBwPXNwRkRSLnJhbmRvbSRyZXMkUFZhbHVlLCBhZGpwPXNwRkRSLnJhbmRvbSRzcEZEUiwgYWRqcF9mZHI9c3BGRFIucmFuZG9tJHJlcyRGRFIsIGxvZ0ZDPXNwRkRSLnJhbmRvbSRyZXMkbG9nRkMsIFNpZz1zcEZEUi5yYW5kb20kcmVzJFNpZykKZmRyLmRmLnJlZmluZWQgPC0gZGF0YS5mcmFtZShWZXJ0ZXg9YXMuaW50ZWdlcihyb3duYW1lcyhzcEZEUi5yZWZpbmVkJHJlcykpLCBwPXNwRkRSLnJlZmluZWQkcmVzJFBWYWx1ZSwgYWRqcD1zcEZEUi5yZWZpbmVkJHNwRkRSLCBsb2dGQz1zcEZEUi5yZWZpbmVkJHJlcyRsb2dGQywgYWRqcF9mZHI9c3BGRFIucmVmaW5lZCRyZXMkRkRSLCBTaWc9c3BGRFIucmVmaW5lZCRyZXMkU2lnKQoKbWV0YS5kZiAlPiUKICBsZWZ0X2pvaW4oZmRyLmRmLnJhbmRvbSkgJT4lCiAgIyBkcGx5cjo6YXJyYW5nZShzYW1wbGVkKSAlPiUKICBnZ3Bsb3QoYWVzKFVNQVAxLCBVTUFQMiwgCiAgICAgICAgICAgICAjIGNvbG9yPSAtIGxvZzEwKGFkanApLAogICAgICAgICAgICAjIGNvbG9yPSAtIGxvZzEwKHApLAogICAgICAgICAgICAgY29sb3IgPSBsb2dGQwogICAgICAgICAgICAgKSkgKwogIGdlb21fcG9pbnQoc2l6ZT0wLjUpICsKICBnZW9tX3BvaW50KGRhdGE9LiAlPiUgZHBseXI6OmZpbHRlcighaXMubmEoYWRqcCkpKSArCiAgdGhlbWVfY2xlYW4oKSArCiAgc2NhbGVfY29sb3JfZ3JhZGllbnQyKG1pZHBvaW50ID0gMCwgaGlnaCA9ICJyZWQiLCBsb3c9ImJsdWUiLG5hLnZhbHVlID0iZ3JleTgwIikgKwogIGdndGl0bGUoIlJhbmRvbSBzYW1wbGluZyIpCgoKbWV0YS5kZiAlPiUKICBsZWZ0X2pvaW4oZmRyLmRmLnJlZmluZWQpICU+JQogICMgZHBseXI6OmFycmFuZ2Uoc2FtcGxlZCkgJT4lCiAgZ2dwbG90KGFlcyhVTUFQMSwgVU1BUDIsIAogICAgICAgICAgICAgIyBjb2xvcj0gLSBsb2cxMChhZGpwKSwKICAgICAgICAgICAgIyBjb2xvcj0gLSBsb2cxMChwKSwKICAgICAgICAgICAgIGNvbG9yID0gbG9nRkMKICAgICAgICAgICAgICkpICsKICBnZW9tX3BvaW50KHNpemU9MC41KSArCiAgZ2VvbV9wb2ludChkYXRhPS4gJT4lIGRwbHlyOjpmaWx0ZXIoIWlzLm5hKGFkanApKSkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG9yX2dyYWRpZW50MihtaWRwb2ludCA9IDAsIGhpZ2ggPSAicmVkIiwgbG93PSJibHVlIixuYS52YWx1ZSA9ImdyZXk4MCIpICsKICBnZ3RpdGxlKCJSZWZpbmVkIHNhbXBsaW5nIikKCmBgYAoKCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBmaWcud2lkdGg9MTAsIGZpZy5oZWlnaHQ9NX0KbWV0YS5kZiAlPiUKICAgbGVmdF9qb2luKGZkci5kZi5yZWZpbmVkKSAlPiUKICBkcGx5cjo6ZmlsdGVyKCFpcy5uYShsb2dGQykpICU+JQogIGdncGxvdChhZXMobG9nRkMsIC1sb2cxMChhZGpwKSwgc2hhcGU9U2lnLCBjb2xvcj1ncm91cF9pZCkpICsKICBnZW9tX3BvaW50KCkgKwogIGdndGl0bGUoInJlZmluZWQgc2FtcGxpbmciKSArCm1ldGEuZGYgJT4lCiAgIGxlZnRfam9pbihmZHIuZGYucmFuZG9tKSAlPiUKICBkcGx5cjo6ZmlsdGVyKCFpcy5uYShsb2dGQykpICU+JQogIGdncGxvdChhZXMobG9nRkMsIC1sb2cxMChhZGpwKSwgc2hhcGU9U2lnLCBjb2xvcj1ncm91cF9pZCkpICsKICBnZW9tX3BvaW50KCkgKwogIGdndGl0bGUoInJhbmRvbSBzYW1wbGluZyIpIApgYGAKCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBmaWcud2lkdGg9MTIsIGZpZy5oZWlnaHQ9NX0KbnVtX21pbGVzdG9uZXM9MTAKCm1ldGEuZGYgJT4lCiAgdW5ncm91cCgpICU+JQogIGxlZnRfam9pbihmZHIuZGYucmVmaW5lZCkgJT4lCiAgZHBseXI6Om11dGF0ZShncm91cF9pZCA9IGZhY3Rvcihncm91cF9pZCwgbGV2ZWxzPXBhc3RlMCgnTScsIDE6bnVtX21pbGVzdG9uZXMpKSkgJT4lCiAgZHBseXI6OmZpbHRlcighaXMubmEobG9nRkMpKSAlPiUKICBnZ3Bsb3QoYWVzKGdyb3VwX2lkLCBsb2dGQywgIHNoYXBlPVNpZywgY29sb3I9Z3JvdXBfaWQsIHNpemU9IC1sb2cxMChhZGpwKSkpICsKICBnZW9tX2ppdHRlcigpICsKICBnZ3RpdGxlKCJyZWZpbmVkIHNhbXBsaW5nIikgKwptZXRhLmRmICU+JQogIHVuZ3JvdXAoKSAlPiUKICBsZWZ0X2pvaW4oZmRyLmRmLnJhbmRvbSkgJT4lCiAgZHBseXI6Om11dGF0ZShncm91cF9pZCA9IGZhY3Rvcihncm91cF9pZCwgbGV2ZWxzPXBhc3RlMCgnTScsIDE6bnVtX21pbGVzdG9uZXMpKSkgJT4lICBkcGx5cjo6ZmlsdGVyKCFpcy5uYShsb2dGQykpICU+JQogIGdncGxvdChhZXMoZ3JvdXBfaWQsIGxvZ0ZDLCBzaGFwZT1TaWcsIGNvbG9yPWdyb3VwX2lkLCBzaXplPSAtbG9nMTAoYWRqcCkpKSArCiAgZ2VvbV9qaXR0ZXIoKSArCiAgZ2d0aXRsZSgicmFuZG9tIHNhbXBsaW5nIikgCmBgYAoKLS0tCgojIyBIb3cgbWFueSBuZWlnaGJvcmhvb2RzIGRpc2FwcGVhciB3IHJlZmluZW1lbnQ/CgpgYGB7cn0Kc2ltdWxhdGVfbGluZWFyX3RyYWogPC0gZnVuY3Rpb24obnVtX2NlbGxzLCBudW1fbWlsZXN0b25lcywgbnVtX2ZlYXR1cmVzPTEwMDAsIGtfcGFyYW09MjEsIHNlZWQ9NDIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHByb2Jfc3RhcnQ9MC4xLCBwcm9iX2VuZD0wLjkpewogIHNldC5zZWVkKHNlZWQpCiAgIyMgR2VuZXJhdGUgc2ltdWxhdGVkIGRhdGFzZXQgb2YgdHJhamVjdG9yeQogIGRhdGFzZXQgPC0gZ2VuZXJhdGVfZGF0YXNldCgKICAgIG1vZGVsID0gbW9kZWxfbGluZWFyKG51bV9taWxlc3RvbmVzID0gbnVtX21pbGVzdG9uZXMpLAogICAgbnVtX2NlbGxzID0gbnVtX2NlbGxzLAogICAgbnVtX2ZlYXR1cmVzID0gbnVtX2ZlYXR1cmVzCiAgKQogIHNpbTIuZ2V4IDwtIGFzLm1hdHJpeChkYXRhc2V0JGV4cHJlc3Npb24pCiAgc2ltMi5icmFuY2hlcyA8LSBkYXRhc2V0JHByaW9yX2luZm9ybWF0aW9uJGdyb3Vwc19pZAogIHNpbTIudGltZSA9IGRhdGFzZXQkcHJpb3JfaW5mb3JtYXRpb24kdGltZWNvdXJzZV9jb250aW51b3VzCiAgCiAgIyMgQnVpbGQgZ3JhcGggCiAgc2ltMi5wY2EgPC0gcHJjb21wX2lybGJhKHNpbTIuZ2V4LCBuPTUwLCBzY2FsZS49VFJVRSwgY2VudGVyPVRSVUUpCiAgWF9wY2EgPSBzaW0yLnBjYSR4WywgYygxOjMwKV0KICBzaW0yLmtubiA8LSBidWlsZEtOTkdyYXBoKHg9WF9wY2EsIGs9a19wYXJhbSwgZD1OQSwgdHJhbnNwb3NlZD1UUlVFKQogICMjIFJ1biBVTUFQCiAgc3RlbS50YS51bWFwIDwtIHVtYXAoc2ltMi5wY2EkeFssIGMoMTozMCldLAogICAgICAgICAgICAgICAgICAgICAgIG5fY29tcG9uZW50cz0yLAogICAgICAgICAgICAgICAgICAgICAgIG5fbmVpZ2hib3JzPWtfcGFyYW0sIG1ldHJpYz0nZXVjbGlkZWFuJywKICAgICAgICAgICAgICAgICAgICAgICBpbml0PSdyYW5kb20nLCBtaW5fZGlzdD0wLjEpCiAgZHluLmRmIDwtIGRhdGEuZnJhbWUoVU1BUDE9c3RlbS50YS51bWFwJGxheW91dFssMV0sIFVNQVAyPXN0ZW0udGEudW1hcCRsYXlvdXRbLDJdLCAKICAgICAgICAgICAgIGNlbGxfaWQ9cm93bmFtZXMoc2ltMi5nZXgpLCB0aW1lPXNpbTIudGltZSkKICBkeW4uZGYgPC0gZHluLmRmICU+JSBsZWZ0X2pvaW4oc2ltMi5icmFuY2hlcykKICAKICAjIyBTaW11bGF0ZSBjb25kaXRpb25zCiAgbl9ncm91cHMgPC0gbGVuZ3RoKHVuaXF1ZShkeW4uZGYkZ3JvdXBfaWQpKQogIHBfdmVjIDwtIHNlcShwcm9iX3N0YXJ0LCBwcm9iX2VuZCwgbGVuZ3RoLm91dCA9IG5fZ3JvdXBzKQogIGEuY2VsbHMgPC0gYygpCiAgZm9yIChpIGluIDE6bl9ncm91cHMpIHsKICAgIGcgPC0gcGFzdGUwKCJNIixpKQogICAgcCA8LSBwX3ZlY1tpXSAKICAgIG0uQSA8LSBzYW1wbGUoZHluLmRmJGNlbGxfaWRbZHluLmRmJGdyb3VwX2lkPT1nXSwgCiAgICAgICAgICAgICAgICAgIHNpemU9Zmxvb3Ioc3VtKGR5bi5kZiRncm91cF9pZD09ZykqcCkpCiAgICBhLmNlbGxzIDwtIGMoYS5jZWxscywgbS5BKQogIH0KICAKICBkeW4uZGYgPC0gZHluLmRmICU+JSBkcGx5cjo6bXV0YXRlKGNvbmRpdGlvbiA9IGlmZWxzZShjZWxsX2lkICVpbiUgYS5jZWxscywgIkEiLCAnQicpKSAKCiAgIyMgU2ltdWxhdGUgcmVwbGljYXRlcwogIGR5bi5kZiA8LSBkeW4uZGYgJT4lCiAgICBncm91cF9ieShncm91cF9pZCkgJT4lCiAgICBkcGx5cjo6bXV0YXRlKHJlcGxpY2F0ZT1jKHJlcCgiUjEiLCBmbG9vcihuKCkqMC4zKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXAoIlIyIiwgZmxvb3IobigpKjAuMykpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwKCJSMyIsIG4oKSAtIDIqKGZsb29yKG4oKSowLjMpKSkpCiAgICApIAogIAogICMjIEFkZCBzYW1wbGUgbmFtZSAoY29uZGl0aW9uICsgcmVwbGljYXRlKQogIGR5bi5kZiRzYW1wbGUgPC0gcGFzdGUoZHluLmRmJGNvbmRpdGlvbiwgZHluLmRmJHJlcGxpY2F0ZSwgc2VwPSJfIikKICAjIyBBZGQgdmVydGV4IGlkIChmb3IgY291bnRzKQogIGR5bi5kZiRWZXJ0ZXggPC0gYXMudmVjdG9yKFYoc2ltMi5rbm4pKQogIAogICMjIE1ha2UgbW9kZWwgbWF0cml4IGZvciB0ZXN0aW5nCiAgc2FtcGxlLm1ldGEgPC0gZGF0YS5mcmFtZSgiQ29uZGl0aW9uIj1jKHJlcCgiQSIsIDMpLCByZXAoIkIiLCAzKSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAiUmVwbGljYXRlIj1yZXAoYygiUjEiLCAiUjIiLCAiUjMiKSwgMikpCiAgc2FtcGxlLm1ldGEkU2FtcGxlIDwtIHBhc3RlKHNhbXBsZS5tZXRhJENvbmRpdGlvbiwgc2FtcGxlLm1ldGEkUmVwbGljYXRlLCBzZXA9Il8iKQogIHJvd25hbWVzKHNhbXBsZS5tZXRhKSA8LSBzYW1wbGUubWV0YSRTYW1wbGUKICBzaW0yLm1vZGVsIDwtIG1vZGVsLm1hdHJpeCh+IDAgKyBDb25kaXRpb24sIGRhdGE9c2FtcGxlLm1ldGEpCiAgCiAgcmV0dXJuKGxpc3QoZ3JhcGg9c2ltMi5rbm4sCiAgICAgICAgICAgICAgWF9wY2E9WF9wY2EsCiAgICAgICAgICAgICAgbWV0YS5kZj1keW4uZGYsCiAgICAgICAgICAgICAgbW9kZWw9c2ltMi5tb2RlbCkpCiAgCiAgfQpgYGAKCgpgYGB7ciwgZmlnLndpZHRoPTEyLCBmaWcuaGVpZ2h0PTV9CmRhdGFfMmtfY2VsbHMgPC0gc2ltdWxhdGVfbGluZWFyX3RyYWoobnVtX2NlbGxzID0gMjAwMCwgbnVtX21pbGVzdG9uZXMgPSAxMCwgcHJvYl9zdGFydCA9IDAuNSwgcHJvYl9lbmQ9MC45NSkKCmdyYXBoIDwtIGRhdGFfMmtfY2VsbHMkZ3JhcGgKc2FtcGxlLnZlcnRpY2VzIDwtIDAuMQptZXRhLmRmIDwtIGRhdGFfMmtfY2VsbHMkbWV0YS5kZgptb2RlbCA8LSBkYXRhXzJrX2NlbGxzJG1vZGVsClhfcGNhIDwtIGRhdGFfMmtfY2VsbHMkWF9wY2EKCnJhbmRvbS52ZXJ0aWNlcyA8LSBzYW1wbGUoVihncmFwaCksIHNpemU9Zmxvb3Ioc2FtcGxlLnZlcnRpY2VzKmxlbmd0aChWKGdyYXBoKSkpKQp2ZXJ0ZXgua25uIDwtIEJpb2NOZWlnaGJvcnM6OmZpbmRLTk4oWD1YX3BjYSwgaz0yMSwgc3Vic2V0PWFzLnZlY3RvcihyYW5kb20udmVydGljZXMpKQpyZWZpbmVkLnZlcnRpY2VzIDwtIFYoZ3JhcGgpW3NhcHBseSgxOm5yb3codmVydGV4LmtubiRpbmRleCksIGZ1bmN0aW9uKGkpIHJlZmluZV92ZXJ0ZXgodmVydGV4LmtubiwgaSwgWF9wY2EpKV0KCgpkYXRhLmZyYW1lKHJhbmRvbT1hcy5udW1lcmljKHJhbmRvbS52ZXJ0aWNlcyksIHJlZmluZWQ9YXMubnVtZXJpYyhyZWZpbmVkLnZlcnRpY2VzKSkgJT4lCiAgcm93aWRfdG9fY29sdW1uKCkgJT4lCiAgZ3JvdXBfYnkoYXMuZmFjdG9yKHJlZmluZWQpKSAlPiUKICBkcGx5cjo6bXV0YXRlKG5fY29udmVyZ2luZyA9IG4oKSkgJT4lCiAgdW5ncm91cCgpICU+JQogIHBpdm90X2xvbmdlcihjb2xzPWMoJ3JhbmRvbScsICJyZWZpbmVkIiksIG5hbWVzX3RvID0gInNhbXBsaW5nX3NjaGVtZSIsIHZhbHVlc190byA9ICJWZXJ0ZXgiKSAlPiUKICBsZWZ0X2pvaW4obWV0YS5kZiwgYnk9IlZlcnRleCIpICU+JQogIGdncGxvdChhZXModGltZSxzYW1wbGluZ19zY2hlbWUsIGNvbG9yPW5fY29udmVyZ2luZykpICsKICBnZW9tX3BvaW50KHNpemU9MC41KSArCiAgZ2VvbV9saW5lKGFlcyhncm91cD1yb3dpZCksIHNpemU9MC41KSArCiAgc2NhbGVfY29sb3JfdmlyaWRpc19jKCkKCmBgYApgYGB7cn0KZGF0YS5mcmFtZShyYW5kb209YXMubnVtZXJpYyhyYW5kb20udmVydGljZXMpLCByZWZpbmVkPWFzLm51bWVyaWMocmVmaW5lZC52ZXJ0aWNlcykpICU+JQogIHJvd2lkX3RvX2NvbHVtbigpICU+JQogIGdyb3VwX2J5KGFzLmZhY3RvcihyZWZpbmVkKSkgJT4lCiAgZHBseXI6Om11dGF0ZShuX2NvbnZlcmdpbmcgPSBuKCkpICU+JQogIGdncGxvdChhZXMoYXMuZmFjdG9yKG5fY29udmVyZ2luZykpKSArIGdlb21faGlzdG9ncmFtKHN0YXQ9ImNvdW50IikKYGBgCgojIyMgQWZ0ZXIgcmVmaW5lbWVudCB3aGF0IGlzIHRoZSBkaXN0YW5jZSB0byB0aGUgbmVhcmVzdCBzYW1wbGVkIGNlbGw/IApEaXN0YW5jZXMgc2hvdWxkIGJlY29tZSBtb3JlIHVuaWZvcm0KCmBgYHtyLCBmaWcud2lkdGg9MTIsIGZpZy5oZWlnaHQ9NX0KZ2V0X2Rpc3RfdG9fY2xvc2VzdF9uZWlnaCA8LSBmdW5jdGlvbihncmFwaCwgc2FtcGxlLnZlcnRpY2VzKXsKICByYW5kb20udmVydGljZXMgPC0gc2FtcGxlKFYoZ3JhcGgpLCBzaXplPWZsb29yKHNhbXBsZS52ZXJ0aWNlcypsZW5ndGgoVihncmFwaCkpKSkKICB2ZXJ0ZXgua25uIDwtIEJpb2NOZWlnaGJvcnM6OmZpbmRLTk4oWD1YX3BjYSwgaz0yMSwgc3Vic2V0PWFzLnZlY3RvcihyYW5kb20udmVydGljZXMpKQogIHJlZmluZWQudmVydGljZXMgPC0gVihncmFwaClbc2FwcGx5KDE6bnJvdyh2ZXJ0ZXgua25uJGluZGV4KSwgZnVuY3Rpb24oaSkgcmVmaW5lX3ZlcnRleCh2ZXJ0ZXgua25uLCBpLCBYX3BjYSkpXQogIGRpc3RfdG9fY2xvc2VzdF9yYW5kb20gPC0gQmlvY05laWdoYm9yczo6ZmluZEtOTihYPVhfcGNhW2FzLnZlY3RvcihyYW5kb20udmVydGljZXMpLF0sIGs9MSlbWyJkaXN0YW5jZSJdXQogIGRpc3RfdG9fY2xvc2VzdF9yZWZpbmVkIDwtIEJpb2NOZWlnaGJvcnM6OmZpbmRLTk4oWD1YX3BjYVt1bmlxdWUoYXMudmVjdG9yKHJlZmluZWQudmVydGljZXMpKSxdLCBrPTEpW1siZGlzdGFuY2UiXV0KICBkaXN0X2RmIDwtIGJpbmRfcm93cyhkYXRhLmZyYW1lKGRpc3RhbmNlX3RvX2Nsb3Nlc3Q9ZGlzdF90b19jbG9zZXN0X3JlZmluZWQsIHNhbXBsaW5nX3NjaGVtZT0ncmVmaW5lZCcpLAogICAgICAgICAgZGF0YS5mcmFtZShkaXN0YW5jZV90b19jbG9zZXN0PWRpc3RfdG9fY2xvc2VzdF9yYW5kb20sIHNhbXBsaW5nX3NjaGVtZT0ncmFuZG9tJykpICU+JQogICAgZHBseXI6Om11dGF0ZShzYW1wbGVfcGVyYz1zYW1wbGUudmVydGljZXMpCn0KCmRpc3RfbHMgPC0gbWFwKHNlcSgwLjEsMC42LCBieSA9IDAuMDUpLCB+IGdldF9kaXN0X3RvX2Nsb3Nlc3RfbmVpZ2goZ3JhcGgsIC54KSkgCnB1cnJyOjpyZWR1Y2UoZGlzdF9scywgYmluZF9yb3dzKSAlPiUKICBnZ3Bsb3QoYWVzKGFzLmZhY3RvcihzYW1wbGVfcGVyYyksIGRpc3RhbmNlX3RvX2Nsb3Nlc3QsIGNvbG9yPXNhbXBsaW5nX3NjaGVtZSkpICsKICAjIGdnYmVlc3dhcm06Omdlb21fcXVhc2lyYW5kb20oKQogIGdlb21fYm94cGxvdCh2YXJ3aWR0aCA9IFRSVUUpICsKICB4bGFiKCIlIHNhbXBsZWQiKSArIHlsYWIoIkRpc3RhbmNlIHRvIGNsb3Nlc3Qgc2FtcGxlIikgKwogIHRoZW1lX2dyZXkoYmFzZV9zaXplID0gMTQpCmBgYAoKIyMgV2hhdCBpcyB0aGUgcmVsYXRpb25zaGlwIGJldHdlZW4gbmVpZ2hib3Job29kIHNpemUgYW5kIGs/CgpgYGB7cn0KZGF0YV81a19jZWxscyA8LSBzaW11bGF0ZV9saW5lYXJfdHJhaihudW1fY2VsbHMgPSA1MDAwLCBudW1fbWlsZXN0b25lcyA9IDEwKQpgYGAKCmBgYHtyfQptZXRhLmRmIDwtIGRhdGFfNWtfY2VsbHMkbWV0YS5kZgptb2RlbCA8LSBkYXRhXzVrX2NlbGxzJG1vZGVsClhfcGNhIDwtIGRhdGFfNWtfY2VsbHMkWF9wY2EKCmtfdmVjIDwtIHNlcSgxMCw1MCwgYnk9NSkKZ3JhcGhfbHMgPC0gbWFwKGtfdmVjLCB+IGJ1aWxkS05OR3JhcGgoeD1YX3BjYSwgaz0ueCwgZD1OQSwgdHJhbnNwb3NlZD1UUlVFKSkKYGBgCgoKYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0UsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD01fQpnZXRfbmVpZ2hfZGYgPC0gZnVuY3Rpb24oc2FtcGxlZF92ZXJ0aWNlcywgZ3JhcGgsIFhfcGNhLCBrX3BhcmFtLCBzYW1wbGluZ19tb2RlPSJyYW5kb20iKXsKICBpZiAoc2FtcGxpbmdfbW9kZT09InJlZmluZWQiKSB7CiAgICB2ZXJ0ZXgua25uIDwtIEJpb2NOZWlnaGJvcnM6OmZpbmRLTk4oWD1YX3BjYSwgaz1rX3BhcmFtLCBzdWJzZXQ9YXMudmVjdG9yKHNhbXBsZWRfdmVydGljZXMpKQogICAgc2FtcGxlZF92ZXJ0aWNlcyA8LSBWKGdyYXBoKVtzYXBwbHkoMTpucm93KHZlcnRleC5rbm4kaW5kZXgpLCBmdW5jdGlvbihpKSByZWZpbmVfdmVydGV4KHZlcnRleC5rbm4sIGksIFhfcGNhKSldCiAgfQogIHNhbXBsZWRfdmVydGljZXMgPC0gdW5pcXVlKHNhbXBsZWRfdmVydGljZXMpCiAgdmVydGV4Lmxpc3QgPC0gc2FwcGx5KDE6bGVuZ3RoKHNhbXBsZWRfdmVydGljZXMpLCBGVU49ZnVuY3Rpb24oWCkgbmVpZ2hib3JzKGdyYXBoLCB2PXNhbXBsZWRfdmVydGljZXNbWF0pKQogIG5laWdoX2RmIDwtIGRhdGEuZnJhbWUobmVpZ2hfdmVydGV4PWFzLnZlY3RvcihzYW1wbGVkX3ZlcnRpY2VzKSwgbmVpZ2hfc2l6ZT1zYXBwbHkodmVydGV4Lmxpc3QsIGZ1bmN0aW9uKHgpIGxlbmd0aCh4KSksIAogICAgICAgICAgICAgICAgICAgICAgICAgc2FtcGxpbmdfbW9kZT1zYW1wbGluZ19tb2RlLCBrPWtfcGFyYW0pCiAgcmV0dXJuKG5laWdoX2RmKQogIH0KCm5laWdoX2RmX2xzIDwtIGxhcHBseShzZXFfYWxvbmcoa192ZWMpLCBmdW5jdGlvbihpKXsKICByYW5kb21fc2FtcGxlIDwtIHNhbXBsZShWKGdyYXBoX2xzW1tpXV0pLCBzaXplPWZsb29yKHNhbXBsZS52ZXJ0aWNlcypsZW5ndGgoVihncmFwaF9sc1tbaV1dKSkpKQogIHNhbXBsZWRfdmVydGljZXMgPC0gcmFuZG9tX3NhbXBsZQogIHJhbmRvbV9uZWlnaF9kZiA8LSBnZXRfbmVpZ2hfZGYocmFuZG9tX3NhbXBsZSwgZ3JhcGhfbHNbW2ldXSwgWF9wY2EsIGtfdmVjW2ldLCBzYW1wbGluZ19tb2RlPSJyYW5kb20iKQogIHJlZmluZWRfbmVpZ2hfZGYgPC0gZ2V0X25laWdoX2RmKHJhbmRvbV9zYW1wbGUsIGdyYXBoX2xzW1tpXV0sIFhfcGNhLCBrX3ZlY1tpXSwgc2FtcGxpbmdfbW9kZT0icmVmaW5lZCIpCiAgYmluZF9yb3dzKHJhbmRvbV9uZWlnaF9kZiwgcmVmaW5lZF9uZWlnaF9kZikKICB9KQoKcHVycnI6OnJlZHVjZShuZWlnaF9kZl9scywgYmluZF9yb3dzKSAlPiUKICBnZ3Bsb3QoYWVzKGFzLmZhY3RvcihrKSwgbmVpZ2hfc2l6ZSwgY29sb3I9c2FtcGxpbmdfbW9kZSkpICsKICBnZW9tX3Zpb2xpbihzY2FsZSA9ICJ3aWR0aCIpICsKICBnZW9tX2JveHBsb3Qod2lkdGg9MC4yKSArCiAgZmFjZXRfd3JhcChzYW1wbGluZ19tb2Rlfi4pICsKICB4bGFiKCJLIikgKyB5bGFiKCJOZWlnaGJvcmhvb2Qgc2l6ZSIpICsKICB0aGVtZV9jbGVhbihiYXNlX3NpemUgPSAxOCkKCmBgYApgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRSwgZmlnLndpZHRoPTEwLCBmaWcuaGVpZ2h0PTV9CnB1cnJyOjpyZWR1Y2UobmVpZ2hfZGZfbHMsIGJpbmRfcm93cykgJT4lCiAgZ2dwbG90KGFlcyhhcy5mYWN0b3IoayksIG5laWdoX3NpemUgLyBrLCBjb2xvcj1zYW1wbGluZ19tb2RlKSkgKwogIGdlb21fdmlvbGluKHNjYWxlID0gIndpZHRoIikgKwogIGdlb21fYm94cGxvdCh3aWR0aD0wLjIpICsKICBmYWNldF93cmFwKHNhbXBsaW5nX21vZGV+LikgKwogIHhsYWIoIksiKSArIHlsYWIoIk5laWdoYm9yaG9vZCBzaXplIC8gSyIpICsKICB0aGVtZV9jbGVhbihiYXNlX3NpemUgPSAxOCkKCmBgYAoKIyMjIyBSZWxhdGlvbnNoaXAgYmV0d2VlbiBLIGFuZCBuZWlnaGJvcmhvb2Qgc2l6ZQpgYGB7cn0KcHVycnI6OnJlZHVjZShuZWlnaF9kZl9scywgYmluZF9yb3dzKSAlPiUKICBncm91cF9ieShzYW1wbGluZ19tb2RlLCBrKSAlPiUKICBzdW1tYXJpc2Uobj1uKCkpICU+JQogIGdncGxvdChhZXMoayxuLCBjb2xvcj1zYW1wbGluZ19tb2RlKSkgKwogIGdlb21fcG9pbnQoKQpgYGAKCgoKCgoKCgo=
Milo on human thymus
library(SingleCellExperiment)
Loading required package: SummarizedExperiment
Loading required package: GenomicRanges
Loading required package: stats4
Loading required package: BiocGenerics
Loading required package: parallel

Attaching package: ‘BiocGenerics’

The following objects are masked from ‘package:parallel’:

    clusterApply, clusterApplyLB, clusterCall, clusterEvalQ, clusterExport, clusterMap, parApply,
    parCapply, parLapply, parLapplyLB, parRapply, parSapply, parSapplyLB

The following objects are masked from ‘package:stats’:

    IQR, mad, sd, var, xtabs

The following objects are masked from ‘package:base’:

    anyDuplicated, append, as.data.frame, basename, cbind, colnames, dirname, do.call,
    duplicated, eval, evalq, Filter, Find, get, grep, grepl, intersect, is.unsorted, lapply, Map,
    mapply, match, mget, order, paste, pmax, pmax.int, pmin, pmin.int, Position, rank, rbind,
    Reduce, rownames, sapply, setdiff, sort, table, tapply, union, unique, unsplit, which,
    which.max, which.min

Loading required package: S4Vectors

Attaching package: ‘S4Vectors’

The following object is masked from ‘package:base’:

    expand.grid

Loading required package: IRanges
Loading required package: GenomeInfoDb
Loading required package: Biobase
Welcome to Bioconductor

    Vignettes contain introductory material; view with 'browseVignettes()'. To cite Bioconductor,
    see 'citation("Biobase")', and for packages 'citation("pkgname")'.

Loading required package: DelayedArray
Loading required package: matrixStats

Attaching package: ‘matrixStats’

The following objects are masked from ‘package:Biobase’:

    anyMissing, rowMedians


Attaching package: ‘DelayedArray’

The following objects are masked from ‘package:matrixStats’:

    colMaxs, colMins, colRanges, rowMaxs, rowMins, rowRanges

The following objects are masked from ‘package:base’:

    aperm, apply, rowsum
library(tidyverse)
Registered S3 methods overwritten by 'dbplyr':
  method         from
  print.tbl_lazy     
  print.tbl_sql      
── Attaching packages ──────────────────────────────────────────────────────────────────── tidyverse 1.3.0 ──
✓ ggplot2 3.3.2     ✓ purrr   0.3.4
✓ tibble  3.0.3     ✓ dplyr   1.0.1
✓ tidyr   1.1.1     ✓ stringr 1.4.0
✓ readr   1.3.1     ✓ forcats 0.5.0
── Conflicts ─────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
x dplyr::collapse()   masks IRanges::collapse()
x dplyr::combine()    masks Biobase::combine(), BiocGenerics::combine()
x dplyr::count()      masks matrixStats::count()
x dplyr::desc()       masks IRanges::desc()
x tidyr::expand()     masks S4Vectors::expand()
x dplyr::filter()     masks stats::filter()
x dplyr::first()      masks S4Vectors::first()
x dplyr::lag()        masks stats::lag()
x ggplot2::Position() masks BiocGenerics::Position(), base::Position()
x purrr::reduce()     masks GenomicRanges::reduce(), IRanges::reduce()
x dplyr::rename()     masks S4Vectors::rename()
x purrr::simplify()   masks DelayedArray::simplify()
x dplyr::slice()      masks IRanges::slice()
library(igraph)

Attaching package: ‘igraph’

The following objects are masked from ‘package:dplyr’:

    as_data_frame, groups, union

The following objects are masked from ‘package:purrr’:

    compose, simplify

The following object is masked from ‘package:tidyr’:

    crossing

The following object is masked from ‘package:tibble’:

    as_data_frame

The following objects are masked from ‘package:DelayedArray’:

    path, simplify

The following object is masked from ‘package:GenomicRanges’:

    union

The following object is masked from ‘package:IRanges’:

    union

The following object is masked from ‘package:S4Vectors’:

    union

The following objects are masked from ‘package:BiocGenerics’:

    normalize, path, union

The following objects are masked from ‘package:stats’:

    decompose, spectrum

The following object is masked from ‘package:base’:

    union
library(scran)

devtools::load_all("~/miloR/")
import scanpy as sc
import pandas as pd
import numpy as np
## To show plots inline
import matplotlib.pyplot as plt
import sys,os
plt.switch_backend('agg')
sys.path.insert(1, '/nfs/team205/ed6/bin/thATAC/preprocess_utils/')
import atac_utils 
import scipy.sparse
from pathlib import Path

# sc._settings.ScanpyConfig.figdir = Path(r.outdir)

Load data

Load anndata object, downloaded from here following the link from Park et al. 2020

rna_adata = sc.read_h5ad("/nfs/team205/ed6/data/Park_scRNAseq/HTA08.v01.A06.Science_human_tcells.raw.h5ad")
rna_adata.X = rna_adata.raw.X
rna_adata.X = scipy.sparse.csc_matrix(rna_adata.X)

Load MOFA projection, to use as reduced dimensionality object

mofa_dims <- read.csv("/nfs/team205/ed6/data/thymus_data/thymus_MOFA_projection.csv") %>%
  column_to_rownames("cell")

## Filter just the scRNA-seq cells and factors 4 knn graoh construciton
mofa_dims <- as.matrix(mofa_dims[rownames(py$rna_adata$obs),1:5])

Convert anndata to SingleCellExperiment object

adata <- py$rna_adata
cnt <- t(adata$X)
rownames(cnt) <- adata$var_names$to_list()
colnames(cnt) <- adata$obs_names$to_list()
logCnt <- log2(cnt + 1)
pca <- prcomp(t(vx))

# Create the SingleCellExperiment object
sce <- SingleCellExperiment(assay=list(counts=cnt, logcounts=logCnt), colData = adata$obs)

reducedDim(sce) <- mofa_dims
reducedDimNames(sce) <- "MOFA"
sce

## Save SingleCellExperiment
saveRDS(sce, "/nfs/team205/ed6/data/Park_scRNAseq/HTA08.v01.A06.Science_human_tcells.SingleCellExperiment.RDS")
sce <- readRDS("~/Downloads/HTA08.v01.A06.Science_human_tcells.SingleCellExperiment.RDS")
sce
class: SingleCellExperiment 
dim: 33694 50514 
metadata(0):
assays(2): counts logcounts
rownames(33694): TSPAN6 TNMD ... RP11-107E5.4 RP11-299P2.2
rowData names(0):
colnames(50514): FCAImmP7179369-AAACCTGAGCCCAATT FCAImmP7179369-AAACCTGAGCCTATGT ... Human_colon_16S7985397-TTTGCGCAGAGCCCAA
  Human_colon_16S7985397-TTTGGTTAGTACGCGA
colData names(14): Sample donor ... cell types umap_density_Age
reducedDimNames(1): MOFA
altExpNames(0):
object.size(sce)
16486280 bytes

Cells in different samples where sorted using different FACS gates, which affect significantly the cell type composition of different samples. To simplify interpretation of DA analysis, I retain only samples that where obtained from total tissue and CD45+ cells, which have a similar cell type composition.

sce
class: SingleCellExperiment 
dim: 33694 38081 
metadata(0):
assays(2): counts logcounts
rownames(33694): TSPAN6 TNMD ... RP11-107E5.4 RP11-299P2.2
rowData names(0):
colnames(38081): FCAImmP7179369-AAACCTGAGCCCAATT FCAImmP7179369-AAACCTGAGCCTATGT ... Human_colon_16S7985397-TTTGCGCAGAGCCCAA
  Human_colon_16S7985397-TTTGGTTAGTACGCGA
colData names(14): Sample donor ... cell types umap_density_Age
reducedDimNames(1): MOFA
altExpNames(0):

Make Milo object

milo <- Milo(sce)
milo
class: Milo 
dim: 33694 38081 
metadata(0):
assays(2): counts logcounts
rownames(33694): TSPAN6 TNMD ... RP11-107E5.4 RP11-299P2.2
rowData names(0):
colnames(38081): FCAImmP7179369-AAACCTGAGCCCAATT FCAImmP7179369-AAACCTGAGCCTATGT ... Human_colon_16S7985397-TTTGCGCAGAGCCCAA
  Human_colon_16S7985397-TTTGGTTAGTACGCGA
colData names(14): Sample donor ... cell types umap_density_Age
reducedDimNames(1): MOFA
altExpNames(0):
neighbourhoods dimensions(1): 0
neighbourhoodCounts dimensions(2): 1 1
neighbourDistances dimensions(2): 1 1
graph names(0):
neighbourhoodIndex names(1): 0
object.size(milo)
13060216 bytes

Build KNN graph

For now I use scran function instead of buildGraph from package because it’s very slow

## Rename MOFA dim reduction as PCA so buildGraph can find it
reducedDim(milo, "PCA") <- reducedDim(milo)
# 
# library(BiocNeighbors)
# library(BiocParallel)
# milo <- buildGraph(milo, k = 30)

knn_graph <- buildKNNGraph(reducedDim(milo, "MOFA"), k=20, d=NA, transposed=TRUE)
miloR::graph(milo) <- knn_graph

Run umap

umap_th <- uwot::umap(reducedDim(milo, "MOFA"), n_neighbors=20 )
reducedDim(milo, 'UMAP') <- umap_th
scater::plotUMAP(milo, colour_by="Age", point_size=0.5, point_alpha=0.5) +
  facet_wrap('colour_by')

Test for differential abundance by age

Simple case: compare first time point with last one

small_milo <- milo[,which(milo$Age %in% c('7w','17w'))]
knn_graph <- buildKNNGraph(reducedDim(small_milo, "MOFA"), k=30, d=NA, transposed=TRUE)
miloR::graph(small_milo) <- knn_graph

Run umap

small_umap_th <- uwot::umap(reducedDim(small_milo, "MOFA"), n_neighbors=30 )
reducedDim(small_milo, 'UMAP') <- small_umap_th
scater::plotUMAP(small_milo, colour_by="Age", point_size=0.5, point_alpha=0.5) +
  facet_wrap('colour_by')

Sample neighborhoods with refined sampling scheme

# milo@neighbourhoods <- list()
small_milo <- makeNeighbourhoods(small_milo, prop=0.1, k = 30, d=5, refined = TRUE, reduced_dims = "PCA", seed = 100)
Checking valid object
plotNeighborhoodSizeHist(small_milo, bins=100)

Make model matrix for testing. I use Age as an ordinal variable for testing.

th.meta <- data.frame(colData(small_milo)[,c("Sample","Age")]) 
th.meta$Age <- ordered(th.meta$Age, levels=c('7w','17w'))
th.meta <-
  distinct(th.meta) %>%
  rownames_to_column() %>%
  select(Sample, Age) %>%
  column_to_rownames("Sample")

th.meta %>%
  # filter(Age=="16w")
  ggplot(aes(Age)) + geom_bar()


th.model <- model.matrix(~  Age, data=th.meta)
th.model
                (Intercept)      Age.L
F29_TH_45P                1  0.7071068
F29_TH_45P_5GEX           1  0.7071068
C40_TH_TOT_1              1 -0.7071068
C40_TH_TOT_2              1 -0.7071068
attr(,"assign")
[1] 0 1
attr(,"contrasts")
attr(,"contrasts")$Age
[1] "contr.poly"
small_milo <- countCells(small_milo, 
                   data = data.frame(colData(small_milo)[,c("Sample","Age")]),
                   samples = "Sample")
Checking data validity
Setting up matrix with 765 neighbourhoods
Counting cells in neighbourhoods
graph_spatialFDR <- function(neighborhoods, graph, pvalues, connectivity='vertex', pca=NULL){
  # input a set of neighborhoods as a list of graph vertices
  # the input graph and the unadjusted GLM p-values
  #' neighborhoods: list of vertices and their respective neighborhoods
  #' graph: input kNN graph
  #' pvalues: a vector of pvalues in the same order as the neighborhood indices
  #' connectivity: character - edge or vertex to calculate neighborhood connectivity or distance to use average Euclidean distance
  #' pca: matrix of PCs to calculate Euclidean distances, only required when connectivity == distance
  # Discarding NA pvalues.
  haspval <- !is.na(pvalues)
  if (!all(haspval)) {
      coords <- coords[haspval, , drop=FALSE]
      pvalues <- pvalues[haspval]
  }
    
  # define the subgraph for each neighborhood then calculate the vertex connectivity for each
  # this latter computation is quite slow - can it be sped up?
  subgraphs <- lapply(1:length(neighborhoods[haspval]),
                         FUN=function(X) induced_subgraph(graph, neighborhoods[haspval][[X]]))
  # now loop over these sub-graphs to calculate the connectivity - this seems a little slow...
  if(connectivity == "vertex"){
    t.connect <- lapply(subgraphs, FUN=function(EG) vertex_connectivity(EG))
  } else if(connectivity == "edge"){
    t.connect <- lapply(subgraphs, FUN=function(EG) edge_connectivity(EG))
  } else if(connectivity == "distance"){
    if(!is.null(pca)){
      t.connect <- lapply(1:length(neighborhoods[haspval]),
                        FUN=function(PG) {
                          x.pcs <- pca[neighborhoods[haspval][[PG]], ]
                          x.euclid <- as.matrix(dist(x.pcs))
                          x.distdens <- 1/mean(x.euclid[lower.tri(x.euclid, diag=FALSE)])
                        return(x.distdens)})
    } else{
      stop("A matrix of PCs is required to calculate distances")  
    }
  }else{
    stop("connectivity option not recognised - must be either edge, vertex or distance")
  }
  
  # use 1/connectivity as the weighting for the weighted BH adjustment from Cydar
  w <- 1/unlist(t.connect)
  w[is.infinite(w)] <- 0
  
  # Computing a density-weighted q-value.
  o <- order(pvalues)
  pvalues <- pvalues[o]
  w <- w[o]
  adjp <- numeric(length(o))
  adjp[o] <- rev(cummin(rev(sum(w)*pvalues/cumsum(w))))
  adjp <- pmin(adjp, 1)
  if (!all(haspval)) {
    refp <- rep(NA_real_, length(haspval))
    refp[haspval] <- adjp
    adjp <- refp
    }
  return(adjp)
}

# testQLF <- function(graph, nh_counts, th.model, connectivity='edge', pca=NULL){
nh_counts <- small_milo@neighbourhoodCounts

dge <- DGEList(nh_counts[, rownames(th.model)], lib.size=log(colSums(nh_counts)))
dge <- estimateDisp(dge, th.model)

fit <- glmQLFit(dge, th.model, robust=TRUE)
# sim2.contrast <- makeContrasts(ConditionA - ConditionB, levels=th.model)
#   sim2.res <- glmQLFTest(sim2.fit, contrast=sim2.contrast)
milo_res <- as.data.frame(topTags(glmQLFTest(fit, coef=1), sort.by='none', n=Inf))
milo_res$Sig <- as.factor(as.numeric(milo_res$FDR <= 0.05))
milo_res$Neighbourhood <- as.numeric(rownames(milo_res))

sim2.spatialfdr <- graph_spatialFDR(neighborhoods=small_milo@neighbourhoods, 
                                    graph=small_milo@graph[["graph"]],
                                    connectivity="distance", 
                                    pvalues=milo_res$PValue,
                                    pca=reducedDim(milo,"MOFA")
                                    )
milo_res_df <- data.frame(Vertex=names(small_milo@neighbourhoods),
                          p=milo_res$PValue, 
                          adjp=sim2.spatialfdr, 
                          logFC=milo_res$logFC, 
                          adjp_fdr=milo_res$FDR, 
                          Sig=milo_res$Sig
                          )


milo_res_df %>%
  mutate(is_sig=ifelse(adjp < 0.1, TRUE, FALSE)) %>%
  ggplot(aes(logFC, -log10(adjp), color=is_sig)) +
  geom_point(size=0.1)

# colData(small_milo) <- colData(sce[,which(sce$Age %in% c('7w','17w'))])
colData(small_milo)["Vertex"] <- as.character(V(graph(small_milo)))
coldata_df <-
  SingleCellExperiment::colData(small_milo) %>%
  as.data.frame() %>%
  rownames_to_column() %>%
  left_join(milo_res_df) 
Joining, by = "Vertex"
coldata_df
# colData(small_milo)[which(small_milo$Vertex=="850"),]
#   milo_res_df %>%
#     filter(logFC > 0) %>% pull(Vertex)
  
# colnames(milo_res_df) %in% colnames(colData(small_milo))
coldata_df <- bind_cols(coldata_df, data.frame(reducedDim(small_milo, 'UMAP'))) 

coldata_df %>%
  ggplot(aes(X1, X2)) +
  geom_point(aes(color=Age), size=0.5)

Use Age as ordinal variable w all ages

knn_graph <- buildKNNGraph(reducedDim(milo, "MOFA"), k=30, d=NA, transposed=TRUE)
miloR::graph(milo) <- knn_graph

Run umap

umap_th <- uwot::umap(reducedDim(milo, "MOFA"), n_neighbors=30, verbose=TRUE)
reducedDim(milo, 'UMAP') <- umap_th

Sample neighborhoods with refined sampling scheme

# milo@neighbourhoods <- list()
system.time(milo <- makeNeighbourhoods(milo, prop=0.1, k = 30, d=5, refined = TRUE, reduced_dims = "PCA", seed = 100))
Checking valid object
   user  system elapsed 
 56.819   1.309  58.305 

Make model matrix for testing. I use Age as an ordinal variable for testing.

th.model
                  (Intercept)       Age.L       Age.Q      Age.C       Age^4       Age^5      Age^6       Age^7       Age^8        Age^9
F21_TH_45P                  1  0.38533732  0.17407766 -0.1511417 -0.41137668 -0.50128041 -0.4281744 -0.27517866 -0.13089258 -0.040816431
F22_TH_TOT                  1 -0.27524094 -0.08703883  0.3778543 -0.31788198 -0.03580574  0.3892495 -0.50351840  0.37397880 -0.163265726
F23_TH_45P                  1 -0.05504819 -0.34815531  0.1295501  0.33658092 -0.21483446 -0.3113996  0.32787245  0.26178516 -0.571430041
F29_TH_45P                  1  0.49543369  0.52223297  0.4534252  0.33658092  0.21483446  0.1167748  0.05269379  0.01869894  0.004535159
F29_TH_45P_5GEX             1  0.49543369  0.52223297  0.4534252  0.33658092  0.21483446  0.1167748  0.05269379  0.01869894  0.004535159
F30_TH_45P                  1  0.27524094 -0.08703883 -0.3778543 -0.31788198  0.03580574  0.3892495  0.50351840  0.37397880  0.163265726
F30_TH_45P_5GEX             1  0.27524094 -0.08703883 -0.3778543 -0.31788198  0.03580574  0.3892495  0.50351840  0.37397880  0.163265726
F38_TH_45P                  1  0.16514456 -0.26111648 -0.3346710  0.05609682  0.39386318  0.2335497 -0.24590434 -0.52357031 -0.380953360
F38_TH_45P_5GEX             1  0.16514456 -0.26111648 -0.3346710  0.05609682  0.39386318  0.2335497 -0.24590434 -0.52357031 -0.380953360
F41_TH_45P                  1  0.38533732  0.17407766 -0.1511417 -0.41137668 -0.50128041 -0.4281744 -0.27517866 -0.13089258 -0.040816431
F41_TH_45P_5GEX             1  0.38533732  0.17407766 -0.1511417 -0.41137668 -0.50128041 -0.4281744 -0.27517866 -0.13089258 -0.040816431
F45_TH_45P                  1  0.05504819 -0.34815531 -0.1295501  0.33658092  0.21483446 -0.3113996 -0.32787245  0.26178516  0.571430041
F45_TH_45P_5GEX             1  0.05504819 -0.34815531 -0.1295501  0.33658092  0.21483446 -0.3113996 -0.32787245  0.26178516  0.571430041
F64_TH_TOT_5GEX_1           1 -0.05504819 -0.34815531  0.1295501  0.33658092 -0.21483446 -0.3113996  0.32787245  0.26178516 -0.571430041
F64_TH_TOT_5GEX_2           1 -0.05504819 -0.34815531  0.1295501  0.33658092 -0.21483446 -0.3113996  0.32787245  0.26178516 -0.571430041
C34_TH_TOT_5GEX             1 -0.27524094 -0.08703883  0.3778543 -0.31788198 -0.03580574  0.3892495 -0.50351840  0.37397880 -0.163265726
C40_TH_TOT_1                1 -0.49543369  0.52223297 -0.4534252  0.33658092 -0.21483446  0.1167748 -0.05269379  0.01869894 -0.004535159
C40_TH_TOT_2                1 -0.49543369  0.52223297 -0.4534252  0.33658092 -0.21483446  0.1167748 -0.05269379  0.01869894 -0.004535159
C41_TH_TOT_1                1 -0.38533732  0.17407766  0.1511417 -0.41137668  0.50128041 -0.4281744  0.27517866 -0.13089258  0.040816431
C41_TH_TOT_2                1 -0.38533732  0.17407766  0.1511417 -0.41137668  0.50128041 -0.4281744  0.27517866 -0.13089258  0.040816431
F74_TH_TOT_5GEX_1           1 -0.16514456 -0.26111648  0.3346710  0.05609682 -0.39386318  0.2335497  0.24590434 -0.52357031  0.380953360
F74_TH_TOT_5GEX_2           1 -0.16514456 -0.26111648  0.3346710  0.05609682 -0.39386318  0.2335497  0.24590434 -0.52357031  0.380953360
attr(,"assign")
 [1] 0 1 1 1 1 1 1 1 1 1
attr(,"contrasts")
attr(,"contrasts")$Age
[1] "contr.poly"
heatmap(as.numeric(neighbourhoodCounts(milo)))
Error in heatmap(as.numeric(neighbourhoodCounts(milo))) : 
  'x' must be a numeric matrix
nh_counts <- milo@neighbourhoodCounts

dge <- DGEList(nh_counts[, rownames(th.model)], lib.size=log(colSums(nh_counts)))
dge <- estimateDisp(dge, th.model)

fit <- glmQLFit(dge, th.model, robust=TRUE)
# sim2.contrast <- makeContrasts(ConditionA - ConditionB, levels=th.model)
#   sim2.res <- glmQLFTest(sim2.fit, contrast=sim2.contrast)
milo_res <- as.data.frame(topTags(glmQLFTest(fit, coef=1), sort.by='none', n=Inf))
milo_res$Sig <- as.factor(as.numeric(milo_res$FDR <= 0.05))
milo_res$Neighbourhood <- as.numeric(rownames(milo_res))

sim2.spatialfdr <- graph_spatialFDR(neighborhoods=milo@neighbourhoods, 
                                    graph=milo@graph[["graph"]],
                                    connectivity="distance", 
                                    pvalues=milo_res$PValue,
                                    pca=reducedDim(milo,"MOFA")
                                    )

colData(milo) <- colData(sce)
colData(milo)["Vertex"] <- as.character(V(graph(milo)))
coldata_df <-
  SingleCellExperiment::colData(milo) %>%
  as.data.frame() %>%
  rownames_to_column() %>%
  left_join(milo_res_df) 
Joining, by = "Vertex"
coldata_df
# colData(small_milo)[which(small_milo$Vertex=="850"),]
#   milo_res_df %>%
#     filter(logFC > 0) %>% pull(Vertex)
  
colnames(milo_res_df) %in% colnames(colData(milo))
[1]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE
coldata_df <- bind_cols(coldata_df, data.frame(reducedDim(milo, 'UMAP'))) 
coldata_df %>%
  arrange(- logFC) %>%
  ggplot(aes(X1,X2)) + 
  geom_point(size=0.2, color="grey", alpha=0.5) +
  geom_point(data=. %>% filter(!is.na(logFC)), aes(color=logFC), size=1) +
  # scale_color_gradient2(mid=0, high = "red", low="blue") +
  scale_color_viridis_c(option="magma") +
  theme_dimred()


coldata_df %>%
  arrange(- log10(adjp)) %>%
  ggplot(aes(X1,X2)) + 
  geom_point(size=0.2, color="grey", alpha=0.5) +
  geom_point(data=. %>% filter(!is.na(adjp)), aes(color=-log10(adjp))) +
  scale_color_viridis_c() +
  theme_dimred()

coldata_df %>%
  arrange(nh_size) %>%
  ggplot(aes(X1,X2)) + 
  geom_point(size=0.2, color="grey", alpha=0.5) +
  geom_point(data=. %>% filter(!is.na(adjp)), aes(color=nh_size)) +
  scale_color_viridis_c() +
  theme_dimred()

NA
NA
coldata_df %>%
  arrange(- logFC) %>%
  ggplot(aes(X1,X2)) + 
  geom_point(size=0.2, color="grey", alpha=0.5) +
  geom_point(data=. %>% filter(!is.na(logFC)), aes(color=logFC), size=1) +
  # scale_color_gradient2(mid=0, high = "red", low="blue") +
  scale_color_viridis_c(option="magma", direction=-1) +
  facet_wrap(cell.types~.) +
  theme_dimred()

coldata_df %>%
  ggplot(aes(cell.types, fill=Age)) + geom_bar(position="fill") +
  scale_fill_viridis_d() +
  coord_flip()

coldata_df %>%
  group_by(Age) %>%
  mutate(n_ct=n()) %>%
  ungroup() %>%
  group_by(cell.types, Age) %>%
  summarise(frac_ct=n()/n_ct) %>%
  ggplot(aes(Age, frac_ct, color=cell.types)) + 
  geom_point() + 
  geom_line(aes(group=cell.types)) +
  facet_wrap(cell.types~.)
`summarise()` regrouping output by 'cell.types', 'Age' (override with `.groups` argument)

Filtering just Agex for which you have > 2 samples

th.meta <- data.frame(colData(sce)[,c("Sample","Age")]) 
keep.ages <- c('11w','12w','13w','14w','16w','17w')

th.meta$Age <- ordered(th.meta$Age, levels=c('7w','8w','9w','10w','11w','12w','13w','14w','16w','17w'))
th.meta <-
  distinct(th.meta) %>%
  filter(Age %in% keep.ages) %>%
  mutate(Age = ordered(Age, levels=keep.ages)) %>%
  rownames_to_column() %>%
  select(Sample, Age) %>%
  column_to_rownames("Sample")

th.meta %>%
  ggplot(aes(Age)) + geom_bar()

th.model <- model.matrix(~  Age, data=th.meta)
th.model
# milo_filt <- milo[,which(milo$Age %in% keep.ages)]

milo <- countCells(milo, 
                   data = data.frame(colData(milo)[,c("Sample","Age")]),
                   samples = "Sample")


milo@neighbourhoodCounts
graph_spatialFDR <- function(neighborhoods, graph, pvalues, connectivity='vertex', pca=NULL){
  # input a set of neighborhoods as a list of graph vertices
  # the input graph and the unadjusted GLM p-values
  #' neighborhoods: list of vertices and their respective neighborhoods
  #' graph: input kNN graph
  #' pvalues: a vector of pvalues in the same order as the neighborhood indices
  #' connectivity: character - edge or vertex to calculate neighborhood connectivity or distance to use average Euclidean distance
  #' pca: matrix of PCs to calculate Euclidean distances, only required when connectivity == distance
  # Discarding NA pvalues.
  haspval <- !is.na(pvalues)
  if (!all(haspval)) {
      coords <- coords[haspval, , drop=FALSE]
      pvalues <- pvalues[haspval]
  }
    
  # define the subgraph for each neighborhood then calculate the vertex connectivity for each
  # this latter computation is quite slow - can it be sped up?
  subgraphs <- lapply(1:length(neighborhoods[haspval]),
                         FUN=function(X) induced_subgraph(graph, neighborhoods[haspval][[X]]))
  # now loop over these sub-graphs to calculate the connectivity - this seems a little slow...
  if(connectivity == "vertex"){
    t.connect <- lapply(subgraphs, FUN=function(EG) vertex_connectivity(EG))
  } else if(connectivity == "edge"){
    t.connect <- lapply(subgraphs, FUN=function(EG) edge_connectivity(EG))
  } else if(connectivity == "distance"){
    if(!is.null(pca)){
      t.connect <- lapply(1:length(neighborhoods[haspval]),
                        FUN=function(PG) {
                          x.pcs <- pca[neighborhoods[haspval][[PG]], ]
                          x.euclid <- as.matrix(dist(x.pcs))
                          x.distdens <- 1/mean(x.euclid[lower.tri(x.euclid, diag=FALSE)])
                        return(x.distdens)})
    } else{
      stop("A matrix of PCs is required to calculate distances")  
    }
  }else{
    stop("connectivity option not recognised - must be either edge, vertex or distance")
  }
  
  # use 1/connectivity as the weighting for the weighted BH adjustment from Cydar
  w <- 1/unlist(t.connect)
  w[is.infinite(w)] <- 0
  
  # Computing a density-weighted q-value.
  o <- order(pvalues)
  pvalues <- pvalues[o]
  w <- w[o]
  adjp <- numeric(length(o))
  adjp[o] <- rev(cummin(rev(sum(w)*pvalues/cumsum(w))))
  adjp <- pmin(adjp, 1)
  if (!all(haspval)) {
    refp <- rep(NA_real_, length(haspval))
    refp[haspval] <- adjp
    adjp <- refp
    }
  return(adjp)
}

# testQLF <- function(graph, nh_counts, th.model, connectivity='edge', pca=NULL){
nh_counts <- milo@neighbourhoodCounts

dge <- DGEList(nh_counts[, rownames(th.model)], lib.size=log(colSums(nh_counts)))
dge <- estimateDisp(dge, th.model)

fit <- glmQLFit(dge, th.model, robust=TRUE)
# sim2.contrast <- makeContrasts(ConditionA - ConditionB, levels=th.model)
#   sim2.res <- glmQLFTest(sim2.fit, contrast=sim2.contrast)
milo_res <- as.data.frame(topTags(glmQLFTest(fit, coef=1), sort.by='none', n=Inf))
milo_res$Sig <- as.factor(as.numeric(milo_res$FDR <= 0.05))
milo_res$Neighbourhood <- as.numeric(rownames(milo_res))
  
sim2.spatialfdr <- graph_spatialFDR(neighborhoods=milo@neighbourhoods, 
                                    graph=milo@graph[["graph"]],
                                    connectivity="distance", 
                                    pvalues=milo_res$PValue,
                                    pca=reducedDim(milo,"MOFA")
                                    )

Picking k parameter

test_mean_nh_size <- function(m, prop, k, n_cells, d=30){
  m <- m[,sample(colnames(m), size = n_cells)]
  m <- buildGraph(m, k = k, d = d)
  refined_nh <- neighbourhoods(makeNeighbourhoods(m, prop=prop, k=k, d=d, refined = TRUE, seed=42))
  return(mean(sapply(refined_nh, length)))
}

k_vec <- seq(10,50, by=5)
ncells_vec <- round(ncol(milo)*seq(0.1,1, by=0.1),0)

grid_df <- expand.grid(ncells_vec, k_vec)
colnames(grid_df) <- c("n_cells", "k")
mean_nh_sizes_th <- apply(grid_df, 1, function(x) test_mean_nh_size(milo, prop = 0.2, x["k"], x["n_cells"], d=5))

test_mean_nh_size(milo, prop = 0.2, grid_df[1,"k"], grid_df[1,"n_cells"], d=5)
LS0tCnRpdGxlOiAiTWlsbyBvbiBodW1hbiB0aHltdXMiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCmBgYHtyfQpsaWJyYXJ5KFNpbmdsZUNlbGxFeHBlcmltZW50KQpsaWJyYXJ5KHRpZHl2ZXJzZSkKbGlicmFyeShpZ3JhcGgpCmxpYnJhcnkoc2NyYW4pCgpkZXZ0b29sczo6bG9hZF9hbGwoIn4vbWlsb1IvIikKCiMjIFNldHVwIHRvIHVzZSBweXRob24gaW4gUm1kCmxpYnJhcnkocmV0aWN1bGF0ZSkKcmV0aWN1bGF0ZTo6dXNlX2NvbmRhZW52KCJlbW1hX2VudiIpCgp0aGVtZV9kaW1yZWQgPC0gZnVuY3Rpb24oIC4uLiApewogIHRoZW1lKGF4aXMudGlja3MgPWVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0ID0gZWxlbWVudF9ibGFuaygpLCBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0PTAuNSksIC4uLiApCiAgfQpgYGAKCmBgYHtweXRob24sIGVjaG89VFJVRSwgZXZhbD1GQUxTRX0KaW1wb3J0IHNjYW5weSBhcyBzYwppbXBvcnQgcGFuZGFzIGFzIHBkCmltcG9ydCBudW1weSBhcyBucAojIyBUbyBzaG93IHBsb3RzIGlubGluZQppbXBvcnQgbWF0cGxvdGxpYi5weXBsb3QgYXMgcGx0CmltcG9ydCBzeXMsb3MKcGx0LnN3aXRjaF9iYWNrZW5kKCdhZ2cnKQpzeXMucGF0aC5pbnNlcnQoMSwgJy9uZnMvdGVhbTIwNS9lZDYvYmluL3RoQVRBQy9wcmVwcm9jZXNzX3V0aWxzLycpCmltcG9ydCBhdGFjX3V0aWxzIAppbXBvcnQgc2NpcHkuc3BhcnNlCmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aAoKIyBzYy5fc2V0dGluZ3MuU2NhbnB5Q29uZmlnLmZpZ2RpciA9IFBhdGgoci5vdXRkaXIpCmBgYAoKCiMjIyBMb2FkIGRhdGEKCkxvYWQgYW5uZGF0YSBvYmplY3QsIGRvd25sb2FkZWQgZnJvbSBbaGVyZV0oaHR0cHM6Ly96ZW5vZG8ub3JnL3JlY29yZC8zNTcyNDIyIy5Yc1kyaDVOS2hRSSkgZm9sbG93aW5nIHRoZSBsaW5rIGZyb20gW1BhcmsgZXQgYWwuIDIwMjBdKDEwLjExMjYvc2NpZW5jZS5hYXkzMjI0KQoKYGBge3B5dGhvbiwgZWNobz1UUlVFLCBldmFsPUZBTFNFfQpybmFfYWRhdGEgPSBzYy5yZWFkX2g1YWQoIi9uZnMvdGVhbTIwNS9lZDYvZGF0YS9QYXJrX3NjUk5Bc2VxL0hUQTA4LnYwMS5BMDYuU2NpZW5jZV9odW1hbl90Y2VsbHMucmF3Lmg1YWQiKQpybmFfYWRhdGEuWCA9IHJuYV9hZGF0YS5yYXcuWApybmFfYWRhdGEuWCA9IHNjaXB5LnNwYXJzZS5jc2NfbWF0cml4KHJuYV9hZGF0YS5YKQpgYGAKCkxvYWQgTU9GQSBwcm9qZWN0aW9uLCB0byB1c2UgYXMgcmVkdWNlZCBkaW1lbnNpb25hbGl0eSBvYmplY3QKCmBgYHtyLCBlY2hvPVRSVUUsIGV2YWw9RkFMU0V9Cm1vZmFfZGltcyA8LSByZWFkLmNzdigiL25mcy90ZWFtMjA1L2VkNi9kYXRhL3RoeW11c19kYXRhL3RoeW11c19NT0ZBX3Byb2plY3Rpb24uY3N2IikgJT4lCiAgY29sdW1uX3RvX3Jvd25hbWVzKCJjZWxsIikKCiMjIEZpbHRlciBqdXN0IHRoZSBzY1JOQS1zZXEgY2VsbHMgYW5kIGZhY3RvcnMgNCBrbm4gZ3Jhb2ggY29uc3RydWNpdG9uCm1vZmFfZGltcyA8LSBhcy5tYXRyaXgobW9mYV9kaW1zW3Jvd25hbWVzKHB5JHJuYV9hZGF0YSRvYnMpLDE6NV0pCmBgYAoKQ29udmVydCBgYW5uZGF0YWAgdG8gYFNpbmdsZUNlbGxFeHBlcmltZW50YCBvYmplY3QgCgpgYGB7ciwgZWNobz1UUlVFLCBldmFsPUZBTFNFfQphZGF0YSA8LSBweSRybmFfYWRhdGEKY250IDwtIHQoYWRhdGEkWCkKcm93bmFtZXMoY250KSA8LSBhZGF0YSR2YXJfbmFtZXMkdG9fbGlzdCgpCmNvbG5hbWVzKGNudCkgPC0gYWRhdGEkb2JzX25hbWVzJHRvX2xpc3QoKQpsb2dDbnQgPC0gbG9nMihjbnQgKyAxKQpwY2EgPC0gcHJjb21wKHQodngpKQoKIyBDcmVhdGUgdGhlIFNpbmdsZUNlbGxFeHBlcmltZW50IG9iamVjdApzY2UgPC0gU2luZ2xlQ2VsbEV4cGVyaW1lbnQoYXNzYXk9bGlzdChjb3VudHM9Y250LCBsb2djb3VudHM9bG9nQ250KSwgY29sRGF0YSA9IGFkYXRhJG9icykKCnJlZHVjZWREaW0oc2NlKSA8LSBtb2ZhX2RpbXMKcmVkdWNlZERpbU5hbWVzKHNjZSkgPC0gIk1PRkEiCnNjZQoKIyMgU2F2ZSBTaW5nbGVDZWxsRXhwZXJpbWVudApzYXZlUkRTKHNjZSwgIi9uZnMvdGVhbTIwNS9lZDYvZGF0YS9QYXJrX3NjUk5Bc2VxL0hUQTA4LnYwMS5BMDYuU2NpZW5jZV9odW1hbl90Y2VsbHMuU2luZ2xlQ2VsbEV4cGVyaW1lbnQuUkRTIikKYGBgCgpgYGB7cn0Kc2NlIDwtIHJlYWRSRFMoIn4vRG93bmxvYWRzL0hUQTA4LnYwMS5BMDYuU2NpZW5jZV9odW1hbl90Y2VsbHMuU2luZ2xlQ2VsbEV4cGVyaW1lbnQuUkRTIikKc2NlCm9iamVjdC5zaXplKHNjZSkKYGBgCgpDZWxscyBpbiBkaWZmZXJlbnQgc2FtcGxlcyB3aGVyZSBzb3J0ZWQgdXNpbmcgZGlmZmVyZW50IEZBQ1MgZ2F0ZXMsIHdoaWNoIGFmZmVjdCBzaWduaWZpY2FudGx5IHRoZSBjZWxsIHR5cGUgY29tcG9zaXRpb24gb2YgZGlmZmVyZW50IHNhbXBsZXMuIFRvIHNpbXBsaWZ5IGludGVycHJldGF0aW9uIG9mIERBIGFuYWx5c2lzLCBJIHJldGFpbiBvbmx5IHNhbXBsZXMgdGhhdCB3aGVyZSBvYnRhaW5lZCBmcm9tIHRvdGFsIHRpc3N1ZSBhbmQgQ0Q0NSsgY2VsbHMsIHdoaWNoIGhhdmUgYSBzaW1pbGFyIGNlbGwgdHlwZSBjb21wb3NpdGlvbi4KCmBgYHtyfQpjb2xEYXRhKHNjZSkgJT4lCiAgYXMuZGF0YS5mcmFtZSgpICU+JQogIGdncGxvdChhZXMoU2FtcGxlLCBmaWxsPWNlbGwudHlwZXMpKSArCiAgZ2VvbV9iYXIocG9zaXRpb249ImZpbGwiKSArCiAgY29vcmRfZmxpcCgpICsKICBmYWNldF9ncmlkKHNvcnR+Liwgc2NhbGVzPSJmcmVlX3kiLCBzcGFjZT0iZnJlZSIpCmBgYAoKCgpgYGB7cn0Ka2VlcF9jZWxscyA8LSB3aGljaChzY2Ukc29ydCAlaW4lIGMoIlRPVCIsICI0NVAiKSkKc2NlIDwtIHNjZVssa2VlcF9jZWxsc10Kc2NlCmBgYAoKCk1ha2UgTWlsbyBvYmplY3QKYGBge3J9Cm1pbG8gPC0gTWlsbyhzY2UpCm1pbG8Kb2JqZWN0LnNpemUobWlsbykKYGBgCgoKIyMgQnVpbGQgS05OIGdyYXBoCgpGb3Igbm93IEkgdXNlIHNjcmFuIGZ1bmN0aW9uIGluc3RlYWQgb2YgYGJ1aWxkR3JhcGhgIGZyb20gcGFja2FnZSBiZWNhdXNlIGl0J3MgdmVyeSBzbG93CgpgYGB7cn0KIyMgUmVuYW1lIE1PRkEgZGltIHJlZHVjdGlvbiBhcyBQQ0Egc28gYnVpbGRHcmFwaCBjYW4gZmluZCBpdApyZWR1Y2VkRGltKG1pbG8sICJQQ0EiKSA8LSByZWR1Y2VkRGltKG1pbG8pCiMgCiMgbGlicmFyeShCaW9jTmVpZ2hib3JzKQojIGxpYnJhcnkoQmlvY1BhcmFsbGVsKQojIG1pbG8gPC0gYnVpbGRHcmFwaChtaWxvLCBrID0gMzApCgprbm5fZ3JhcGggPC0gYnVpbGRLTk5HcmFwaChyZWR1Y2VkRGltKG1pbG8sICJNT0ZBIiksIGs9MjAsIGQ9TkEsIHRyYW5zcG9zZWQ9VFJVRSkKbWlsb1I6OmdyYXBoKG1pbG8pIDwtIGtubl9ncmFwaApgYGAKClJ1biB1bWFwCmBgYHtyfQp1bWFwX3RoIDwtIHV3b3Q6OnVtYXAocmVkdWNlZERpbShtaWxvLCAiTU9GQSIpLCBuX25laWdoYm9ycz0yMCApCnJlZHVjZWREaW0obWlsbywgJ1VNQVAnKSA8LSB1bWFwX3RoCmBgYAoKYGBge3IsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD0xMH0Kc2NhdGVyOjpwbG90VU1BUChtaWxvLCBjb2xvdXJfYnk9IkFnZSIsIHBvaW50X3NpemU9MC41LCBwb2ludF9hbHBoYT0wLjUpICsKICBmYWNldF93cmFwKCdjb2xvdXJfYnknKQpgYGAKCiMjIFRlc3QgZm9yIGRpZmZlcmVudGlhbCBhYnVuZGFuY2UgYnkgYWdlCgojIyMgU2ltcGxlIGNhc2U6IGNvbXBhcmUgZmlyc3QgdGltZSBwb2ludCB3aXRoIGxhc3Qgb25lCgpgYGB7cn0Kc21hbGxfbWlsbyA8LSBtaWxvWyx3aGljaChtaWxvJEFnZSAlaW4lIGMoJzd3JywnMTd3JykpXQpgYGAKCmBgYHtyfQprbm5fZ3JhcGggPC0gYnVpbGRLTk5HcmFwaChyZWR1Y2VkRGltKHNtYWxsX21pbG8sICJNT0ZBIiksIGs9MzAsIGQ9TkEsIHRyYW5zcG9zZWQ9VFJVRSkKbWlsb1I6OmdyYXBoKHNtYWxsX21pbG8pIDwtIGtubl9ncmFwaApgYGAKClJ1biB1bWFwCmBgYHtyfQpzbWFsbF91bWFwX3RoIDwtIHV3b3Q6OnVtYXAocmVkdWNlZERpbShzbWFsbF9taWxvLCAiTU9GQSIpLCBuX25laWdoYm9ycz0zMCApCnJlZHVjZWREaW0oc21hbGxfbWlsbywgJ1VNQVAnKSA8LSBzbWFsbF91bWFwX3RoCmBgYAoKYGBge3IsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD01fQpzY2F0ZXI6OnBsb3RVTUFQKHNtYWxsX21pbG8sIGNvbG91cl9ieT0iQWdlIiwgcG9pbnRfc2l6ZT0wLjUsIHBvaW50X2FscGhhPTAuNSkgKwogIGZhY2V0X3dyYXAoJ2NvbG91cl9ieScpCmBgYAoKU2FtcGxlIG5laWdoYm9yaG9vZHMgd2l0aCByZWZpbmVkIHNhbXBsaW5nIHNjaGVtZQoKYGBge3J9CiMgbWlsb0BuZWlnaGJvdXJob29kcyA8LSBsaXN0KCkKc21hbGxfbWlsbyA8LSBtYWtlTmVpZ2hib3VyaG9vZHMoc21hbGxfbWlsbywgcHJvcD0wLjEsIGsgPSAzMCwgZD01LCByZWZpbmVkID0gVFJVRSwgcmVkdWNlZF9kaW1zID0gIlBDQSIsIHNlZWQgPSAxMDApCgoKcGxvdE5laWdoYm9yaG9vZFNpemVIaXN0KHNtYWxsX21pbG8sIGJpbnM9MTAwKQoKYGBgCgoKTWFrZSBtb2RlbCBtYXRyaXggZm9yIHRlc3RpbmcuIEkgdXNlIEFnZSBhcyBhbiBvcmRpbmFsIHZhcmlhYmxlIGZvciB0ZXN0aW5nLgpgYGB7cn0KdGgubWV0YSA8LSBkYXRhLmZyYW1lKGNvbERhdGEoc21hbGxfbWlsbylbLGMoIlNhbXBsZSIsIkFnZSIpXSkgCnRoLm1ldGEkQWdlIDwtIG9yZGVyZWQodGgubWV0YSRBZ2UsIGxldmVscz1jKCc3dycsJzE3dycpKQp0aC5tZXRhIDwtCiAgZGlzdGluY3QodGgubWV0YSkgJT4lCiAgcm93bmFtZXNfdG9fY29sdW1uKCkgJT4lCiAgc2VsZWN0KFNhbXBsZSwgQWdlKSAlPiUKICBjb2x1bW5fdG9fcm93bmFtZXMoIlNhbXBsZSIpCgp0aC5tZXRhICU+JQogICMgZmlsdGVyKEFnZT09IjE2dyIpCiAgZ2dwbG90KGFlcyhBZ2UpKSArIGdlb21fYmFyKCkKCnRoLm1vZGVsIDwtIG1vZGVsLm1hdHJpeCh+ICBBZ2UsIGRhdGE9dGgubWV0YSkKdGgubW9kZWwKYGBgCgoKYGBge3J9CnNtYWxsX21pbG8gPC0gY291bnRDZWxscyhzbWFsbF9taWxvLCAKICAgICAgICAgICAgICAgICAgIGRhdGEgPSBkYXRhLmZyYW1lKGNvbERhdGEoc21hbGxfbWlsbylbLGMoIlNhbXBsZSIsIkFnZSIpXSksCiAgICAgICAgICAgICAgICAgICBzYW1wbGVzID0gIlNhbXBsZSIpCgpgYGAKCmBgYHtyfQpncmFwaF9zcGF0aWFsRkRSIDwtIGZ1bmN0aW9uKG5laWdoYm9yaG9vZHMsIGdyYXBoLCBwdmFsdWVzLCBjb25uZWN0aXZpdHk9J3ZlcnRleCcsIHBjYT1OVUxMKXsKICAjIGlucHV0IGEgc2V0IG9mIG5laWdoYm9yaG9vZHMgYXMgYSBsaXN0IG9mIGdyYXBoIHZlcnRpY2VzCiAgIyB0aGUgaW5wdXQgZ3JhcGggYW5kIHRoZSB1bmFkanVzdGVkIEdMTSBwLXZhbHVlcwogICMnIG5laWdoYm9yaG9vZHM6IGxpc3Qgb2YgdmVydGljZXMgYW5kIHRoZWlyIHJlc3BlY3RpdmUgbmVpZ2hib3Job29kcwogICMnIGdyYXBoOiBpbnB1dCBrTk4gZ3JhcGgKICAjJyBwdmFsdWVzOiBhIHZlY3RvciBvZiBwdmFsdWVzIGluIHRoZSBzYW1lIG9yZGVyIGFzIHRoZSBuZWlnaGJvcmhvb2QgaW5kaWNlcwogICMnIGNvbm5lY3Rpdml0eTogY2hhcmFjdGVyIC0gZWRnZSBvciB2ZXJ0ZXggdG8gY2FsY3VsYXRlIG5laWdoYm9yaG9vZCBjb25uZWN0aXZpdHkgb3IgZGlzdGFuY2UgdG8gdXNlIGF2ZXJhZ2UgRXVjbGlkZWFuIGRpc3RhbmNlCiAgIycgcGNhOiBtYXRyaXggb2YgUENzIHRvIGNhbGN1bGF0ZSBFdWNsaWRlYW4gZGlzdGFuY2VzLCBvbmx5IHJlcXVpcmVkIHdoZW4gY29ubmVjdGl2aXR5ID09IGRpc3RhbmNlCiAgIyBEaXNjYXJkaW5nIE5BIHB2YWx1ZXMuCiAgaGFzcHZhbCA8LSAhaXMubmEocHZhbHVlcykKICBpZiAoIWFsbChoYXNwdmFsKSkgewogICAgICBjb29yZHMgPC0gY29vcmRzW2hhc3B2YWwsICwgZHJvcD1GQUxTRV0KICAgICAgcHZhbHVlcyA8LSBwdmFsdWVzW2hhc3B2YWxdCiAgfQogICAgCiAgIyBkZWZpbmUgdGhlIHN1YmdyYXBoIGZvciBlYWNoIG5laWdoYm9yaG9vZCB0aGVuIGNhbGN1bGF0ZSB0aGUgdmVydGV4IGNvbm5lY3Rpdml0eSBmb3IgZWFjaAogICMgdGhpcyBsYXR0ZXIgY29tcHV0YXRpb24gaXMgcXVpdGUgc2xvdyAtIGNhbiBpdCBiZSBzcGVkIHVwPwogIHN1YmdyYXBocyA8LSBsYXBwbHkoMTpsZW5ndGgobmVpZ2hib3Job29kc1toYXNwdmFsXSksCiAgICAgICAgICAgICAgICAgICAgICAgICBGVU49ZnVuY3Rpb24oWCkgaW5kdWNlZF9zdWJncmFwaChncmFwaCwgbmVpZ2hib3Job29kc1toYXNwdmFsXVtbWF1dKSkKICAjIG5vdyBsb29wIG92ZXIgdGhlc2Ugc3ViLWdyYXBocyB0byBjYWxjdWxhdGUgdGhlIGNvbm5lY3Rpdml0eSAtIHRoaXMgc2VlbXMgYSBsaXR0bGUgc2xvdy4uLgogIGlmKGNvbm5lY3Rpdml0eSA9PSAidmVydGV4Iil7CiAgICB0LmNvbm5lY3QgPC0gbGFwcGx5KHN1YmdyYXBocywgRlVOPWZ1bmN0aW9uKEVHKSB2ZXJ0ZXhfY29ubmVjdGl2aXR5KEVHKSkKICB9IGVsc2UgaWYoY29ubmVjdGl2aXR5ID09ICJlZGdlIil7CiAgICB0LmNvbm5lY3QgPC0gbGFwcGx5KHN1YmdyYXBocywgRlVOPWZ1bmN0aW9uKEVHKSBlZGdlX2Nvbm5lY3Rpdml0eShFRykpCiAgfSBlbHNlIGlmKGNvbm5lY3Rpdml0eSA9PSAiZGlzdGFuY2UiKXsKICAgIGlmKCFpcy5udWxsKHBjYSkpewogICAgICB0LmNvbm5lY3QgPC0gbGFwcGx5KDE6bGVuZ3RoKG5laWdoYm9yaG9vZHNbaGFzcHZhbF0pLAogICAgICAgICAgICAgICAgICAgICAgICBGVU49ZnVuY3Rpb24oUEcpIHsKICAgICAgICAgICAgICAgICAgICAgICAgICB4LnBjcyA8LSBwY2FbbmVpZ2hib3Job29kc1toYXNwdmFsXVtbUEddXSwgXQogICAgICAgICAgICAgICAgICAgICAgICAgIHguZXVjbGlkIDwtIGFzLm1hdHJpeChkaXN0KHgucGNzKSkKICAgICAgICAgICAgICAgICAgICAgICAgICB4LmRpc3RkZW5zIDwtIDEvbWVhbih4LmV1Y2xpZFtsb3dlci50cmkoeC5ldWNsaWQsIGRpYWc9RkFMU0UpXSkKICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuKHguZGlzdGRlbnMpfSkKICAgIH0gZWxzZXsKICAgICAgc3RvcCgiQSBtYXRyaXggb2YgUENzIGlzIHJlcXVpcmVkIHRvIGNhbGN1bGF0ZSBkaXN0YW5jZXMiKSAgCiAgICB9CiAgfWVsc2V7CiAgICBzdG9wKCJjb25uZWN0aXZpdHkgb3B0aW9uIG5vdCByZWNvZ25pc2VkIC0gbXVzdCBiZSBlaXRoZXIgZWRnZSwgdmVydGV4IG9yIGRpc3RhbmNlIikKICB9CiAgCiAgIyB1c2UgMS9jb25uZWN0aXZpdHkgYXMgdGhlIHdlaWdodGluZyBmb3IgdGhlIHdlaWdodGVkIEJIIGFkanVzdG1lbnQgZnJvbSBDeWRhcgogIHcgPC0gMS91bmxpc3QodC5jb25uZWN0KQogIHdbaXMuaW5maW5pdGUodyldIDwtIDAKICAKICAjIENvbXB1dGluZyBhIGRlbnNpdHktd2VpZ2h0ZWQgcS12YWx1ZS4KICBvIDwtIG9yZGVyKHB2YWx1ZXMpCiAgcHZhbHVlcyA8LSBwdmFsdWVzW29dCiAgdyA8LSB3W29dCiAgYWRqcCA8LSBudW1lcmljKGxlbmd0aChvKSkKICBhZGpwW29dIDwtIHJldihjdW1taW4ocmV2KHN1bSh3KSpwdmFsdWVzL2N1bXN1bSh3KSkpKQogIGFkanAgPC0gcG1pbihhZGpwLCAxKQogIGlmICghYWxsKGhhc3B2YWwpKSB7CiAgICByZWZwIDwtIHJlcChOQV9yZWFsXywgbGVuZ3RoKGhhc3B2YWwpKQogICAgcmVmcFtoYXNwdmFsXSA8LSBhZGpwCiAgICBhZGpwIDwtIHJlZnAKICAgIH0KICByZXR1cm4oYWRqcCkKfQoKIyB0ZXN0UUxGIDwtIGZ1bmN0aW9uKGdyYXBoLCBuaF9jb3VudHMsIHRoLm1vZGVsLCBjb25uZWN0aXZpdHk9J2VkZ2UnLCBwY2E9TlVMTCl7Cm5oX2NvdW50cyA8LSBzbWFsbF9taWxvQG5laWdoYm91cmhvb2RDb3VudHMKCmRnZSA8LSBER0VMaXN0KG5oX2NvdW50c1ssIHJvd25hbWVzKHRoLm1vZGVsKV0sIGxpYi5zaXplPWxvZyhjb2xTdW1zKG5oX2NvdW50cykpKQpkZ2UgPC0gZXN0aW1hdGVEaXNwKGRnZSwgdGgubW9kZWwpCgpmaXQgPC0gZ2xtUUxGaXQoZGdlLCB0aC5tb2RlbCwgcm9idXN0PVRSVUUpCiMgc2ltMi5jb250cmFzdCA8LSBtYWtlQ29udHJhc3RzKENvbmRpdGlvbkEgLSBDb25kaXRpb25CLCBsZXZlbHM9dGgubW9kZWwpCiMgICBzaW0yLnJlcyA8LSBnbG1RTEZUZXN0KHNpbTIuZml0LCBjb250cmFzdD1zaW0yLmNvbnRyYXN0KQptaWxvX3JlcyA8LSBhcy5kYXRhLmZyYW1lKHRvcFRhZ3MoZ2xtUUxGVGVzdChmaXQsIGNvZWY9MSksIHNvcnQuYnk9J25vbmUnLCBuPUluZikpCm1pbG9fcmVzJFNpZyA8LSBhcy5mYWN0b3IoYXMubnVtZXJpYyhtaWxvX3JlcyRGRFIgPD0gMC4wNSkpCm1pbG9fcmVzJE5laWdoYm91cmhvb2QgPC0gYXMubnVtZXJpYyhyb3duYW1lcyhtaWxvX3JlcykpCgpzaW0yLnNwYXRpYWxmZHIgPC0gZ3JhcGhfc3BhdGlhbEZEUihuZWlnaGJvcmhvb2RzPXNtYWxsX21pbG9AbmVpZ2hib3VyaG9vZHMsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBncmFwaD1zbWFsbF9taWxvQGdyYXBoW1siZ3JhcGgiXV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbm5lY3Rpdml0eT0iZGlzdGFuY2UiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHZhbHVlcz1taWxvX3JlcyRQVmFsdWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBjYT1yZWR1Y2VkRGltKG1pbG8sIk1PRkEiKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICApCmBgYApgYGB7cn0KbWlsb19yZXNfZGYgPC0gZGF0YS5mcmFtZShWZXJ0ZXg9bmFtZXMoc21hbGxfbWlsb0BuZWlnaGJvdXJob29kcyksCiAgICAgICAgICAgICAgICAgICAgICAgICAgcD1taWxvX3JlcyRQVmFsdWUsIAogICAgICAgICAgICAgICAgICAgICAgICAgIGFkanA9c2ltMi5zcGF0aWFsZmRyLCAKICAgICAgICAgICAgICAgICAgICAgICAgICBsb2dGQz1taWxvX3JlcyRsb2dGQywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgYWRqcF9mZHI9bWlsb19yZXMkRkRSLCAKICAgICAgICAgICAgICAgICAgICAgICAgICBTaWc9bWlsb19yZXMkU2lnCiAgICAgICAgICAgICAgICAgICAgICAgICAgKQoKCm1pbG9fcmVzX2RmICU+JQogIG11dGF0ZShpc19zaWc9aWZlbHNlKGFkanAgPCAwLjEsIFRSVUUsIEZBTFNFKSkgJT4lCiAgZ2dwbG90KGFlcyhsb2dGQywgLWxvZzEwKGFkanApLCBjb2xvcj1pc19zaWcpKSArCiAgZ2VvbV9wb2ludChzaXplPTAuMSkKYGBgCgoKCmBgYHtyfQojIGNvbERhdGEoc21hbGxfbWlsbykgPC0gY29sRGF0YShzY2VbLHdoaWNoKHNjZSRBZ2UgJWluJSBjKCc3dycsJzE3dycpKV0pCmNvbERhdGEoc21hbGxfbWlsbylbIlZlcnRleCJdIDwtIGFzLmNoYXJhY3RlcihWKGdyYXBoKHNtYWxsX21pbG8pKSkKY29sZGF0YV9kZiA8LQogIFNpbmdsZUNlbGxFeHBlcmltZW50Ojpjb2xEYXRhKHNtYWxsX21pbG8pICU+JQogIGFzLmRhdGEuZnJhbWUoKSAlPiUKICByb3duYW1lc190b19jb2x1bW4oKSAlPiUKICBsZWZ0X2pvaW4obWlsb19yZXNfZGYpIAoKY29sZGF0YV9kZgojIGNvbERhdGEoc21hbGxfbWlsbylbd2hpY2goc21hbGxfbWlsbyRWZXJ0ZXg9PSI4NTAiKSxdCiMgICBtaWxvX3Jlc19kZiAlPiUKIyAgICAgZmlsdGVyKGxvZ0ZDID4gMCkgJT4lIHB1bGwoVmVydGV4KQogIAojIGNvbG5hbWVzKG1pbG9fcmVzX2RmKSAlaW4lIGNvbG5hbWVzKGNvbERhdGEoc21hbGxfbWlsbykpCmBgYAoKYGBge3J9CmNvbGRhdGFfZGYgPC0gYmluZF9jb2xzKGNvbGRhdGFfZGYsIGRhdGEuZnJhbWUocmVkdWNlZERpbShzbWFsbF9taWxvLCAnVU1BUCcpKSkgCmBgYAoKCmBgYHtyfQojIHBsX2RmIDwtIHNjYXRlcjo6cGxvdFJlZHVjZWREaW0oc21hbGxfbWlsbyxkaW1yZWQ9IlVNQVAiLCBjb2xvdXJfYnkgPSAibG9nRkMiLCApJGRhdGEgIAoKY29sZGF0YV9kZiAlPiUKICBhcnJhbmdlKC0gbG9nRkMpICU+JQogIGdncGxvdChhZXMoWDEsWDIpKSArIAogIGdlb21fcG9pbnQoc2l6ZT0wLjIsIGNvbG9yPSJncmV5IiwgYWxwaGE9MC41KSArCiAgZ2VvbV9wb2ludChkYXRhPS4gJT4lIGZpbHRlcighaXMubmEobG9nRkMpKSwgYWVzKGNvbG9yPWxvZ0ZDKSwgc2l6ZT0xKSArCiAgIyBzY2FsZV9jb2xvcl9ncmFkaWVudDIobWlkPTAsIGhpZ2ggPSAicmVkIiwgbG93PSJibHVlIikgKwogIHNjYWxlX2NvbG9yX3ZpcmlkaXNfYyhvcHRpb249Im1hZ21hIikgKwogIHRoZW1lX2RpbXJlZCgpCgpjb2xkYXRhX2RmICU+JQogIGFycmFuZ2UoLSBsb2cxMChhZGpwKSkgJT4lCiAgZ2dwbG90KGFlcyhYMSxYMikpICsgCiAgZ2VvbV9wb2ludChzaXplPTAuMiwgY29sb3I9ImdyZXkiLCBhbHBoYT0wLjUpICsKICBnZW9tX3BvaW50KGRhdGE9LiAlPiUgZmlsdGVyKCFpcy5uYShhZGpwKSksIGFlcyhjb2xvcj0tbG9nMTAoYWRqcCkpKSArCiAgc2NhbGVfY29sb3JfdmlyaWRpc19jKCkgKwogIHRoZW1lX2RpbXJlZCgpCgpgYGAKCmBgYHtyfQpjb2xkYXRhX2RmICU+JQogIGdncGxvdChhZXMoWDEsIFgyKSkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG9yPUFnZSksIHNpemU9MC41KQpgYGAKCiMjIyBVc2UgQWdlIGFzIG9yZGluYWwgdmFyaWFibGUgdyBhbGwgYWdlcwoKYGBge3J9Cmtubl9ncmFwaCA8LSBidWlsZEtOTkdyYXBoKHJlZHVjZWREaW0obWlsbywgIk1PRkEiKSwgaz0zMCwgZD1OQSwgdHJhbnNwb3NlZD1UUlVFKQptaWxvUjo6Z3JhcGgobWlsbykgPC0ga25uX2dyYXBoCmBgYAoKUnVuIHVtYXAKYGBge3J9CnVtYXBfdGggPC0gdXdvdDo6dW1hcChyZWR1Y2VkRGltKG1pbG8sICJNT0ZBIiksIG5fbmVpZ2hib3JzPTMwLCB2ZXJib3NlPVRSVUUpCnJlZHVjZWREaW0obWlsbywgJ1VNQVAnKSA8LSB1bWFwX3RoCmBgYAoKYGBge3IsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD01fQpzY2F0ZXI6OnBsb3RVTUFQKG1pbG8sIGNvbG91cl9ieT0iQWdlIiwgcG9pbnRfc2l6ZT0wLjUsIHBvaW50X2FscGhhPTAuNSkgKwogIGZhY2V0X3dyYXAoJ2NvbG91cl9ieScpCmBgYAoKU2FtcGxlIG5laWdoYm9yaG9vZHMgd2l0aCByZWZpbmVkIHNhbXBsaW5nIHNjaGVtZQoKYGBge3J9CiMgbWlsb0BuZWlnaGJvdXJob29kcyA8LSBsaXN0KCkKc3lzdGVtLnRpbWUobWlsbyA8LSBtYWtlTmVpZ2hib3VyaG9vZHMobWlsbywgcHJvcD0wLjEsIGsgPSAzMCwgZD01LCByZWZpbmVkID0gVFJVRSwgcmVkdWNlZF9kaW1zID0gIlBDQSIsIHNlZWQgPSAxMDApKQoKcGxvdE5laWdoYm9yaG9vZFNpemVIaXN0KG1pbG8sIGJpbnM9MTAwKQpgYGAKCgpNYWtlIG1vZGVsIG1hdHJpeCBmb3IgdGVzdGluZy4gSSB1c2UgQWdlIGFzIGFuIG9yZGluYWwgdmFyaWFibGUgZm9yIHRlc3RpbmcuCmBgYHtyfQp0aC5tZXRhIDwtIGRhdGEuZnJhbWUoY29sRGF0YShtaWxvKVssYygiU2FtcGxlIiwiQWdlIildKSAKdGgubWV0YSRBZ2UgPC0gb3JkZXJlZCh0aC5tZXRhJEFnZSwgbGV2ZWxzPWMoJzd3JywgJzh3JywgJzl3JywgJzEwdycsICcxMXcnLCAnMTJ3JywgJzEzdycsICcxNHcnLCAnMTZ3JywgJzE3dycpKQp0aC5tZXRhIDwtCiAgZGlzdGluY3QodGgubWV0YSkgJT4lCiAgcm93bmFtZXNfdG9fY29sdW1uKCkgJT4lCiAgc2VsZWN0KFNhbXBsZSwgQWdlKSAlPiUKICBjb2x1bW5fdG9fcm93bmFtZXMoIlNhbXBsZSIpCgp0aC5tZXRhICU+JQogICMgZmlsdGVyKEFnZT09IjE2dyIpCiAgZ2dwbG90KGFlcyhBZ2UpKSArIGdlb21fYmFyKCkKCnRoLm1vZGVsIDwtIG1vZGVsLm1hdHJpeCh+ICBBZ2UsIGRhdGE9dGgubWV0YSkKdGgubW9kZWwKYGBgCgoKYGBge3J9Cm1pbG8gPC0gY291bnRDZWxscyhtaWxvLCAKICAgICAgICAgICAgICAgICAgIGRhdGEgPSBkYXRhLmZyYW1lKGNvbERhdGEobWlsbylbLGMoIlNhbXBsZSIsIkFnZSIpXSksCiAgICAgICAgICAgICAgICAgICBzYW1wbGVzID0gIlNhbXBsZSIpCgoKYGBgCgpgYGB7cn0KbmhfY291bnRzIDwtIG1pbG9AbmVpZ2hib3VyaG9vZENvdW50cwoKZGdlIDwtIERHRUxpc3QobmhfY291bnRzWywgcm93bmFtZXModGgubW9kZWwpXSwgbGliLnNpemU9bG9nKGNvbFN1bXMobmhfY291bnRzKSkpCmRnZSA8LSBlc3RpbWF0ZURpc3AoZGdlLCB0aC5tb2RlbCkKCmZpdCA8LSBnbG1RTEZpdChkZ2UsIHRoLm1vZGVsLCByb2J1c3Q9VFJVRSkKIyBzaW0yLmNvbnRyYXN0IDwtIG1ha2VDb250cmFzdHMoQ29uZGl0aW9uQSAtIENvbmRpdGlvbkIsIGxldmVscz10aC5tb2RlbCkKIyAgIHNpbTIucmVzIDwtIGdsbVFMRlRlc3Qoc2ltMi5maXQsIGNvbnRyYXN0PXNpbTIuY29udHJhc3QpCm1pbG9fcmVzIDwtIGFzLmRhdGEuZnJhbWUodG9wVGFncyhnbG1RTEZUZXN0KGZpdCwgY29lZj0xKSwgc29ydC5ieT0nbm9uZScsIG49SW5mKSkKbWlsb19yZXMkU2lnIDwtIGFzLmZhY3Rvcihhcy5udW1lcmljKG1pbG9fcmVzJEZEUiA8PSAwLjA1KSkKbWlsb19yZXMkTmVpZ2hib3VyaG9vZCA8LSBhcy5udW1lcmljKHJvd25hbWVzKG1pbG9fcmVzKSkKCnNpbTIuc3BhdGlhbGZkciA8LSBncmFwaF9zcGF0aWFsRkRSKG5laWdoYm9yaG9vZHM9bWlsb0BuZWlnaGJvdXJob29kcywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGdyYXBoPW1pbG9AZ3JhcGhbWyJncmFwaCJdXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY29ubmVjdGl2aXR5PSJkaXN0YW5jZSIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwdmFsdWVzPW1pbG9fcmVzJFBWYWx1ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGNhPXJlZHVjZWREaW0obWlsbywiTU9GQSIpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICkKYGBgCmBgYHtyfQptaWxvX3Jlc19kZiA8LSBkYXRhLmZyYW1lKFZlcnRleD1uYW1lcyhtaWxvQG5laWdoYm91cmhvb2RzKSwKICAgICAgICAgICAgICAgICAgICAgICAgICBwPW1pbG9fcmVzJFBWYWx1ZSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgYWRqcD1zaW0yLnNwYXRpYWxmZHIsIAogICAgICAgICAgICAgICAgICAgICAgICAgIGxvZ0ZDPW1pbG9fcmVzJGxvZ0ZDLCAKICAgICAgICAgICAgICAgICAgICAgICAgICBhZGpwX2Zkcj1taWxvX3JlcyRGRFIsIAogICAgICAgICAgICAgICAgICAgICAgICAgIFNpZz1taWxvX3JlcyRTaWcsCiAgICAgICAgICAgICAgICAgICAgICAgICAgbmhfc2l6ZT1zYXBwbHkobWlsb0BuZWlnaGJvdXJob29kcywgbGVuZ3RoKQopCgoKbWlsb19yZXNfZGYgJT4lCiAgZ2dwbG90KGFlcyhwLCBhZGpwKSkgKyBnZW9tX3BvaW50KCkgKwogIGdlb21fYWJsaW5lKGxpbmV0eXBlPTIpIAoKbWlsb19yZXNfZGYgJT4lCiAgZ2dwbG90KGFlcyhhZGpwKSkgKyBnZW9tX2hpc3RvZ3JhbSgpCgptaWxvX3Jlc19kZiAlPiUKICBtdXRhdGUoaXNfc2lnPWlmZWxzZShwIDwgMC4wNSwgVFJVRSwgRkFMU0UpKSAlPiUKICBnZ3Bsb3QoYWVzKGxvZ0ZDLCAtbG9nMTAoYWRqcCksIGNvbG9yPWlzX3NpZywgc2l6ZT1uaF9zaXplKSkgKwogIGdlb21fcG9pbnQoYWxwaGE9MC4yKQpgYGAKCgoKYGBge3J9CmNvbERhdGEobWlsbykgPC0gY29sRGF0YShzY2UpCmNvbERhdGEobWlsbylbIlZlcnRleCJdIDwtIGFzLmNoYXJhY3RlcihWKGdyYXBoKG1pbG8pKSkKY29sZGF0YV9kZiA8LQogIFNpbmdsZUNlbGxFeHBlcmltZW50Ojpjb2xEYXRhKG1pbG8pICU+JQogIGFzLmRhdGEuZnJhbWUoKSAlPiUKICByb3duYW1lc190b19jb2x1bW4oKSAlPiUKICBsZWZ0X2pvaW4obWlsb19yZXNfZGYpIAoKY29sZGF0YV9kZgojIGNvbERhdGEoc21hbGxfbWlsbylbd2hpY2goc21hbGxfbWlsbyRWZXJ0ZXg9PSI4NTAiKSxdCiMgICBtaWxvX3Jlc19kZiAlPiUKIyAgICAgZmlsdGVyKGxvZ0ZDID4gMCkgJT4lIHB1bGwoVmVydGV4KQogIApjb2xuYW1lcyhtaWxvX3Jlc19kZikgJWluJSBjb2xuYW1lcyhjb2xEYXRhKG1pbG8pKQpgYGAKCmBgYHtyfQpjb2xkYXRhX2RmIDwtIGJpbmRfY29scyhjb2xkYXRhX2RmLCBkYXRhLmZyYW1lKHJlZHVjZWREaW0obWlsbywgJ1VNQVAnKSkpIApgYGAKCgpgYGB7cn0KY29sZGF0YV9kZiAlPiUKICBhcnJhbmdlKC0gbG9nRkMpICU+JQogIGdncGxvdChhZXMoWDEsWDIpKSArIAogIGdlb21fcG9pbnQoc2l6ZT0wLjIsIGNvbG9yPSJncmV5IiwgYWxwaGE9MC41KSArCiAgZ2VvbV9wb2ludChkYXRhPS4gJT4lIGZpbHRlcighaXMubmEobG9nRkMpKSwgYWVzKGNvbG9yPWxvZ0ZDKSwgc2l6ZT0xKSArCiAgIyBzY2FsZV9jb2xvcl9ncmFkaWVudDIobWlkPTAsIGhpZ2ggPSAicmVkIiwgbG93PSJibHVlIikgKwogIHNjYWxlX2NvbG9yX3ZpcmlkaXNfYyhvcHRpb249Im1hZ21hIikgKwogIHRoZW1lX2RpbXJlZCgpCgpjb2xkYXRhX2RmICU+JQogIGFycmFuZ2UoLSBsb2cxMChhZGpwKSkgJT4lCiAgZ2dwbG90KGFlcyhYMSxYMikpICsgCiAgZ2VvbV9wb2ludChzaXplPTAuMiwgY29sb3I9ImdyZXkiLCBhbHBoYT0wLjUpICsKICBnZW9tX3BvaW50KGRhdGE9LiAlPiUgZmlsdGVyKCFpcy5uYShhZGpwKSksIGFlcyhjb2xvcj0tbG9nMTAoYWRqcCkpKSArCiAgc2NhbGVfY29sb3JfdmlyaWRpc19jKCkgKwogIHRoZW1lX2RpbXJlZCgpCgpgYGAKCmBgYHtyfQpjb2xkYXRhX2RmICU+JQogIGFycmFuZ2Uobmhfc2l6ZSkgJT4lCiAgZ2dwbG90KGFlcyhYMSxYMikpICsgCiAgZ2VvbV9wb2ludChzaXplPTAuMiwgY29sb3I9ImdyZXkiLCBhbHBoYT0wLjUpICsKICBnZW9tX3BvaW50KGRhdGE9LiAlPiUgZmlsdGVyKCFpcy5uYShhZGpwKSksIGFlcyhjb2xvcj1uaF9zaXplKSkgKwogIHNjYWxlX2NvbG9yX3ZpcmlkaXNfYygpICsKICB0aGVtZV9kaW1yZWQoKQoKCmBgYApgYGB7ciwgZmlnLmhlaWdodD0xMCwgZmlnLndpZHRoPTEwfQpjb2xkYXRhX2RmICU+JQogIGFycmFuZ2UoLSBsb2dGQykgJT4lCiAgZ2dwbG90KGFlcyhYMSxYMikpICsgCiAgZ2VvbV9wb2ludChzaXplPTAuMiwgY29sb3I9ImdyZXkiLCBhbHBoYT0wLjUpICsKICBnZW9tX3BvaW50KGRhdGE9LiAlPiUgZmlsdGVyKCFpcy5uYShsb2dGQykpLCBhZXMoY29sb3I9bG9nRkMpLCBzaXplPTEpICsKICAjIHNjYWxlX2NvbG9yX2dyYWRpZW50MihtaWQ9MCwgaGlnaCA9ICJyZWQiLCBsb3c9ImJsdWUiKSArCiAgc2NhbGVfY29sb3JfdmlyaWRpc19jKG9wdGlvbj0ibWFnbWEiLCBkaXJlY3Rpb249LTEpICsKICBmYWNldF93cmFwKGNlbGwudHlwZXN+LikgKwogIHRoZW1lX2RpbXJlZCgpCgpgYGAKCmBgYHtyfQpjb2xkYXRhX2RmICU+JQogIGdncGxvdChhZXMoY2VsbC50eXBlcywgZmlsbD1BZ2UpKSArIGdlb21fYmFyKHBvc2l0aW9uPSJmaWxsIikgKwogIHNjYWxlX2ZpbGxfdmlyaWRpc19kKCkgKwogIGNvb3JkX2ZsaXAoKQpgYGAKCgpgYGB7cn0KY29sZGF0YV9kZiAlPiUKICBncm91cF9ieShBZ2UpICU+JQogIG11dGF0ZShuX2N0PW4oKSkgJT4lCiAgdW5ncm91cCgpICU+JQogIGdyb3VwX2J5KGNlbGwudHlwZXMsIEFnZSkgJT4lCiAgc3VtbWFyaXNlKGZyYWNfY3Q9bigpL25fY3QpICU+JQogIGdncGxvdChhZXMoQWdlLCBmcmFjX2N0LCBjb2xvcj1jZWxsLnR5cGVzKSkgKyAKICBnZW9tX3BvaW50KCkgKyAKICBnZW9tX2xpbmUoYWVzKGdyb3VwPWNlbGwudHlwZXMpKSArCiAgZmFjZXRfd3JhcChjZWxsLnR5cGVzfi4pCgpgYGAKCgojIyBGaWx0ZXJpbmcganVzdCBBZ2V4IGZvciB3aGljaCB5b3UgaGF2ZSA+IDIgc2FtcGxlcwoKCmBgYHtyfQp0aC5tZXRhIDwtIGRhdGEuZnJhbWUoY29sRGF0YShzY2UpWyxjKCJTYW1wbGUiLCJBZ2UiKV0pIAprZWVwLmFnZXMgPC0gYygnMTF3JywnMTJ3JywnMTN3JywnMTR3JywnMTZ3JywnMTd3JykKCnRoLm1ldGEkQWdlIDwtIG9yZGVyZWQodGgubWV0YSRBZ2UsIGxldmVscz1jKCc3dycsJzh3JywnOXcnLCcxMHcnLCcxMXcnLCcxMncnLCcxM3cnLCcxNHcnLCcxNncnLCcxN3cnKSkKdGgubWV0YSA8LQogIGRpc3RpbmN0KHRoLm1ldGEpICU+JQogIGZpbHRlcihBZ2UgJWluJSBrZWVwLmFnZXMpICU+JQogIG11dGF0ZShBZ2UgPSBvcmRlcmVkKEFnZSwgbGV2ZWxzPWtlZXAuYWdlcykpICU+JQogIHJvd25hbWVzX3RvX2NvbHVtbigpICU+JQogIHNlbGVjdChTYW1wbGUsIEFnZSkgJT4lCiAgY29sdW1uX3RvX3Jvd25hbWVzKCJTYW1wbGUiKQoKdGgubWV0YSAlPiUKICBnZ3Bsb3QoYWVzKEFnZSkpICsgZ2VvbV9iYXIoKQoKdGgubW9kZWwgPC0gbW9kZWwubWF0cml4KH4gIEFnZSwgZGF0YT10aC5tZXRhKQp0aC5tb2RlbAoKYGBgCgoKYGBge3J9CiMgbWlsb19maWx0IDwtIG1pbG9bLHdoaWNoKG1pbG8kQWdlICVpbiUga2VlcC5hZ2VzKV0KCm1pbG8gPC0gY291bnRDZWxscyhtaWxvLCAKICAgICAgICAgICAgICAgICAgIGRhdGEgPSBkYXRhLmZyYW1lKGNvbERhdGEobWlsbylbLGMoIlNhbXBsZSIsIkFnZSIpXSksCiAgICAgICAgICAgICAgICAgICBzYW1wbGVzID0gIlNhbXBsZSIpCgoKbWlsb0BuZWlnaGJvdXJob29kQ291bnRzCmBgYAoKYGBge3J9CmdyYXBoX3NwYXRpYWxGRFIgPC0gZnVuY3Rpb24obmVpZ2hib3Job29kcywgZ3JhcGgsIHB2YWx1ZXMsIGNvbm5lY3Rpdml0eT0ndmVydGV4JywgcGNhPU5VTEwpewogICMgaW5wdXQgYSBzZXQgb2YgbmVpZ2hib3Job29kcyBhcyBhIGxpc3Qgb2YgZ3JhcGggdmVydGljZXMKICAjIHRoZSBpbnB1dCBncmFwaCBhbmQgdGhlIHVuYWRqdXN0ZWQgR0xNIHAtdmFsdWVzCiAgIycgbmVpZ2hib3Job29kczogbGlzdCBvZiB2ZXJ0aWNlcyBhbmQgdGhlaXIgcmVzcGVjdGl2ZSBuZWlnaGJvcmhvb2RzCiAgIycgZ3JhcGg6IGlucHV0IGtOTiBncmFwaAogICMnIHB2YWx1ZXM6IGEgdmVjdG9yIG9mIHB2YWx1ZXMgaW4gdGhlIHNhbWUgb3JkZXIgYXMgdGhlIG5laWdoYm9yaG9vZCBpbmRpY2VzCiAgIycgY29ubmVjdGl2aXR5OiBjaGFyYWN0ZXIgLSBlZGdlIG9yIHZlcnRleCB0byBjYWxjdWxhdGUgbmVpZ2hib3Job29kIGNvbm5lY3Rpdml0eSBvciBkaXN0YW5jZSB0byB1c2UgYXZlcmFnZSBFdWNsaWRlYW4gZGlzdGFuY2UKICAjJyBwY2E6IG1hdHJpeCBvZiBQQ3MgdG8gY2FsY3VsYXRlIEV1Y2xpZGVhbiBkaXN0YW5jZXMsIG9ubHkgcmVxdWlyZWQgd2hlbiBjb25uZWN0aXZpdHkgPT0gZGlzdGFuY2UKICAjIERpc2NhcmRpbmcgTkEgcHZhbHVlcy4KICBoYXNwdmFsIDwtICFpcy5uYShwdmFsdWVzKQogIGlmICghYWxsKGhhc3B2YWwpKSB7CiAgICAgIGNvb3JkcyA8LSBjb29yZHNbaGFzcHZhbCwgLCBkcm9wPUZBTFNFXQogICAgICBwdmFsdWVzIDwtIHB2YWx1ZXNbaGFzcHZhbF0KICB9CiAgICAKICAjIGRlZmluZSB0aGUgc3ViZ3JhcGggZm9yIGVhY2ggbmVpZ2hib3Job29kIHRoZW4gY2FsY3VsYXRlIHRoZSB2ZXJ0ZXggY29ubmVjdGl2aXR5IGZvciBlYWNoCiAgIyB0aGlzIGxhdHRlciBjb21wdXRhdGlvbiBpcyBxdWl0ZSBzbG93IC0gY2FuIGl0IGJlIHNwZWQgdXA/CiAgc3ViZ3JhcGhzIDwtIGxhcHBseSgxOmxlbmd0aChuZWlnaGJvcmhvb2RzW2hhc3B2YWxdKSwKICAgICAgICAgICAgICAgICAgICAgICAgIEZVTj1mdW5jdGlvbihYKSBpbmR1Y2VkX3N1YmdyYXBoKGdyYXBoLCBuZWlnaGJvcmhvb2RzW2hhc3B2YWxdW1tYXV0pKQogICMgbm93IGxvb3Agb3ZlciB0aGVzZSBzdWItZ3JhcGhzIHRvIGNhbGN1bGF0ZSB0aGUgY29ubmVjdGl2aXR5IC0gdGhpcyBzZWVtcyBhIGxpdHRsZSBzbG93Li4uCiAgaWYoY29ubmVjdGl2aXR5ID09ICJ2ZXJ0ZXgiKXsKICAgIHQuY29ubmVjdCA8LSBsYXBwbHkoc3ViZ3JhcGhzLCBGVU49ZnVuY3Rpb24oRUcpIHZlcnRleF9jb25uZWN0aXZpdHkoRUcpKQogIH0gZWxzZSBpZihjb25uZWN0aXZpdHkgPT0gImVkZ2UiKXsKICAgIHQuY29ubmVjdCA8LSBsYXBwbHkoc3ViZ3JhcGhzLCBGVU49ZnVuY3Rpb24oRUcpIGVkZ2VfY29ubmVjdGl2aXR5KEVHKSkKICB9IGVsc2UgaWYoY29ubmVjdGl2aXR5ID09ICJkaXN0YW5jZSIpewogICAgaWYoIWlzLm51bGwocGNhKSl7CiAgICAgIHQuY29ubmVjdCA8LSBsYXBwbHkoMTpsZW5ndGgobmVpZ2hib3Job29kc1toYXNwdmFsXSksCiAgICAgICAgICAgICAgICAgICAgICAgIEZVTj1mdW5jdGlvbihQRykgewogICAgICAgICAgICAgICAgICAgICAgICAgIHgucGNzIDwtIHBjYVtuZWlnaGJvcmhvb2RzW2hhc3B2YWxdW1tQR11dLCBdCiAgICAgICAgICAgICAgICAgICAgICAgICAgeC5ldWNsaWQgPC0gYXMubWF0cml4KGRpc3QoeC5wY3MpKQogICAgICAgICAgICAgICAgICAgICAgICAgIHguZGlzdGRlbnMgPC0gMS9tZWFuKHguZXVjbGlkW2xvd2VyLnRyaSh4LmV1Y2xpZCwgZGlhZz1GQUxTRSldKQogICAgICAgICAgICAgICAgICAgICAgICByZXR1cm4oeC5kaXN0ZGVucyl9KQogICAgfSBlbHNlewogICAgICBzdG9wKCJBIG1hdHJpeCBvZiBQQ3MgaXMgcmVxdWlyZWQgdG8gY2FsY3VsYXRlIGRpc3RhbmNlcyIpICAKICAgIH0KICB9ZWxzZXsKICAgIHN0b3AoImNvbm5lY3Rpdml0eSBvcHRpb24gbm90IHJlY29nbmlzZWQgLSBtdXN0IGJlIGVpdGhlciBlZGdlLCB2ZXJ0ZXggb3IgZGlzdGFuY2UiKQogIH0KICAKICAjIHVzZSAxL2Nvbm5lY3Rpdml0eSBhcyB0aGUgd2VpZ2h0aW5nIGZvciB0aGUgd2VpZ2h0ZWQgQkggYWRqdXN0bWVudCBmcm9tIEN5ZGFyCiAgdyA8LSAxL3VubGlzdCh0LmNvbm5lY3QpCiAgd1tpcy5pbmZpbml0ZSh3KV0gPC0gMAogIAogICMgQ29tcHV0aW5nIGEgZGVuc2l0eS13ZWlnaHRlZCBxLXZhbHVlLgogIG8gPC0gb3JkZXIocHZhbHVlcykKICBwdmFsdWVzIDwtIHB2YWx1ZXNbb10KICB3IDwtIHdbb10KICBhZGpwIDwtIG51bWVyaWMobGVuZ3RoKG8pKQogIGFkanBbb10gPC0gcmV2KGN1bW1pbihyZXYoc3VtKHcpKnB2YWx1ZXMvY3Vtc3VtKHcpKSkpCiAgYWRqcCA8LSBwbWluKGFkanAsIDEpCiAgaWYgKCFhbGwoaGFzcHZhbCkpIHsKICAgIHJlZnAgPC0gcmVwKE5BX3JlYWxfLCBsZW5ndGgoaGFzcHZhbCkpCiAgICByZWZwW2hhc3B2YWxdIDwtIGFkanAKICAgIGFkanAgPC0gcmVmcAogICAgfQogIHJldHVybihhZGpwKQp9CgojIHRlc3RRTEYgPC0gZnVuY3Rpb24oZ3JhcGgsIG5oX2NvdW50cywgdGgubW9kZWwsIGNvbm5lY3Rpdml0eT0nZWRnZScsIHBjYT1OVUxMKXsKbmhfY291bnRzIDwtIG1pbG9AbmVpZ2hib3VyaG9vZENvdW50cwoKZGdlIDwtIERHRUxpc3QobmhfY291bnRzWywgcm93bmFtZXModGgubW9kZWwpXSwgbGliLnNpemU9bG9nKGNvbFN1bXMobmhfY291bnRzKSkpCmRnZSA8LSBlc3RpbWF0ZURpc3AoZGdlLCB0aC5tb2RlbCkKCmZpdCA8LSBnbG1RTEZpdChkZ2UsIHRoLm1vZGVsLCByb2J1c3Q9VFJVRSkKIyBzaW0yLmNvbnRyYXN0IDwtIG1ha2VDb250cmFzdHMoQ29uZGl0aW9uQSAtIENvbmRpdGlvbkIsIGxldmVscz10aC5tb2RlbCkKIyAgIHNpbTIucmVzIDwtIGdsbVFMRlRlc3Qoc2ltMi5maXQsIGNvbnRyYXN0PXNpbTIuY29udHJhc3QpCm1pbG9fcmVzIDwtIGFzLmRhdGEuZnJhbWUodG9wVGFncyhnbG1RTEZUZXN0KGZpdCwgY29lZj0xKSwgc29ydC5ieT0nbm9uZScsIG49SW5mKSkKbWlsb19yZXMkU2lnIDwtIGFzLmZhY3Rvcihhcy5udW1lcmljKG1pbG9fcmVzJEZEUiA8PSAwLjA1KSkKbWlsb19yZXMkTmVpZ2hib3VyaG9vZCA8LSBhcy5udW1lcmljKHJvd25hbWVzKG1pbG9fcmVzKSkKICAKc2ltMi5zcGF0aWFsZmRyIDwtIGdyYXBoX3NwYXRpYWxGRFIobmVpZ2hib3Job29kcz1taWxvQG5laWdoYm91cmhvb2RzLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZ3JhcGg9bWlsb0BncmFwaFtbImdyYXBoIl1dLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb25uZWN0aXZpdHk9ImRpc3RhbmNlIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHB2YWx1ZXM9bWlsb19yZXMkUFZhbHVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwY2E9cmVkdWNlZERpbShtaWxvLCJNT0ZBIikKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKQoKYGBgCgoKIyMgUGlja2luZyBrIHBhcmFtZXRlcgoKYGBge3J9CnRlc3RfbWVhbl9uaF9zaXplIDwtIGZ1bmN0aW9uKG0sIHByb3AsIGssIG5fY2VsbHMsIGQ9MzApewogIG0gPC0gbVssc2FtcGxlKGNvbG5hbWVzKG0pLCBzaXplID0gbl9jZWxscyldCiAgbSA8LSBidWlsZEdyYXBoKG0sIGsgPSBrLCBkID0gZCkKICByZWZpbmVkX25oIDwtIG5laWdoYm91cmhvb2RzKG1ha2VOZWlnaGJvdXJob29kcyhtLCBwcm9wPXByb3AsIGs9aywgZD1kLCByZWZpbmVkID0gVFJVRSwgc2VlZD00MikpCiAgcmV0dXJuKG1lYW4oc2FwcGx5KHJlZmluZWRfbmgsIGxlbmd0aCkpKQp9CgprX3ZlYyA8LSBzZXEoMTAsNTAsIGJ5PTUpCm5jZWxsc192ZWMgPC0gcm91bmQobmNvbChtaWxvKSpzZXEoMC4xLDEsIGJ5PTAuMSksMCkKCmdyaWRfZGYgPC0gZXhwYW5kLmdyaWQobmNlbGxzX3ZlYywga192ZWMpCmNvbG5hbWVzKGdyaWRfZGYpIDwtIGMoIm5fY2VsbHMiLCAiayIpCm1lYW5fbmhfc2l6ZXNfdGggPC0gYXBwbHkoZ3JpZF9kZiwgMSwgZnVuY3Rpb24oeCkgdGVzdF9tZWFuX25oX3NpemUobWlsbywgcHJvcCA9IDAuMiwgeFsiayJdLCB4WyJuX2NlbGxzIl0sIGQ9NSkpCgp0ZXN0X21lYW5fbmhfc2l6ZShtaWxvLCBwcm9wID0gMC4yLCBncmlkX2RmWzEsImsiXSwgZ3JpZF9kZlsxLCJuX2NlbGxzIl0sIGQ9NSkKYGBgCgoKCg==

Test for DA

Visualize results in embedding

Refined sampling scheme

We adopt the refined sampling strategy applied in Wishbone, and adapted from here. Briefly, to avoid selecting outliers with random sampling, I first randomly select \(n\) cells. For each sampled cell I then identify its k neares neighbors and compute the median profile of the neighbors (in this case the profile in reduced PC space). Then I replace each sampled cell by the cell closest to the median profile of its neighbors.

sim_milo_ref <- makeNeighbourhoods(sim_milo,prop = 0.1, k=20, d=30, refined = TRUE)
Checking valid object

Refined sampling stats

With the refined sampling scheme I select cells with a larger neighbourhood on average.

When \(n\) is large I often end up sampling less than \(n\) cells because for many randomly sampled cells the cell closest to the KNNs is the same.

data.frame(sample_proportion=lapply(seq(0.01,0.5, by = 0.05), function(x) rep(x, 3)) %>% purrr::reduce(c), random = random_n, refined = refined_n) %>%
Warning messages:
1: In readChar(file, size, TRUE) : truncating string with embedded nuls
2: In readChar(file, size, TRUE) : truncating string with embedded nuls
3: In readChar(file, size, TRUE) : truncating string with embedded nuls
4: In readChar(file, size, TRUE) : truncating string with embedded nuls
5: In readChar(file, size, TRUE) : truncating string with embedded nuls
6: In readChar(file, size, TRUE) : truncating string with embedded nuls
7: In readChar(file, size, TRUE) : truncating string with embedded nuls
8: In readChar(file, size, TRUE) : truncating string with embedded nuls
9: In readChar(file, size, TRUE) : truncating string with embedded nuls
10: In readChar(file, size, TRUE) : truncating string with embedded nuls
  pivot_longer(cols = - sample_proportion, names_to = 'sampling', values_to = "n") %>%
  ggplot(aes(sample_proportion, n, color=sampling)) + 
  geom_point(size=1) +
  theme_bw(base_size = 16)

Compare DA testing results

res_ref <- testNeighbourhoods(sim_milo_ref, ~ 1 + condition, data = design_df, fdr.weighting = "k-distance")
Performing spatial FDR correction

Refined sampling seems to be able to identify DA at both ends of the spectrum better

plotMiloReducedDim(sim_milo, res_rand, pt_size=2) + ggtitle("Random sampling") 

plotMiloReducedDim(sim_milo_ref, res_ref, pt_size=2) + ggtitle("Refined sampling")

As expected multiple testing correction is less severe with the refined sample set (less points)

res_rand %>%
  ggplot(aes(-log10(PValue), -log10(SpatialFDR))) +
  geom_abline(linetype=2) +
  geom_point() +
  ggtitle("Refined sampling") 

res_ref %>%
  ggplot(aes(-log10(PValue), -log10(SpatialFDR))) +
  geom_abline(linetype=2) +
  ggtitle("Random sampling") +
  geom_point() 

Picking sampling proportion and k

I want to select these parameters to increase mean nh size

grid_df %>%
  mutate(mean_nh_size=mean_nh_sizes) %>%
  group_by(n_cells, k) %>%
  summarise(mean_nh_size=mean(mean_nh_size)) %>%
  ggplot(aes(k, n_cells)) +
  # geom_point() +
  # geom_line(aes(group=n_cells)) +
  geom_tile(aes(fill=mean_nh_size)) +
  scale_fill_viridis_c() 
`summarise()` regrouping output by 'n_cells' (override with `.groups` argument)

Try using real data (thymus dataset)

dim(reducedDim(milo, "PCA") )
[1] 38081     5
mean_nh_sizes_th <- apply(grid_df, 1, function(x) test_mean_nh_size(milo, prop = 0.2, x["k"], x["n_cells"], d=5))
Constructing kNN graph with k:10
Retrieving distances from 10 nearest neighbours
Checking valid object
Constructing kNN graph with k:10
Retrieving distances from 10 nearest neighbours
Checking valid object
Constructing kNN graph with k:10
Retrieving distances from 10 nearest neighbours
Checking valid object
Constructing kNN graph with k:10
Retrieving distances from 10 nearest neighbours
Checking valid object
Constructing kNN graph with k:10
Retrieving distances from 10 nearest neighbours
Checking valid object
Constructing kNN graph with k:10
Retrieving distances from 10 nearest neighbours
Checking valid object
Constructing kNN graph with k:10
Retrieving distances from 10 nearest neighbours
Checking valid object
Constructing kNN graph with k:10
Retrieving distances from 10 nearest neighbours
Checking valid object
Constructing kNN graph with k:10
Retrieving distances from 10 nearest neighbours
Checking valid object
Constructing kNN graph with k:10
Retrieving distances from 10 nearest neighbours
Checking valid object
Constructing kNN graph with k:15
Retrieving distances from 15 nearest neighbours
Checking valid object
Constructing kNN graph with k:15
Retrieving distances from 15 nearest neighbours
Checking valid object
Constructing kNN graph with k:15
Retrieving distances from 15 nearest neighbours
Checking valid object
Constructing kNN graph with k:15
Retrieving distances from 15 nearest neighbours
Checking valid object
Constructing kNN graph with k:15
Retrieving distances from 15 nearest neighbours
Checking valid object
Constructing kNN graph with k:15
Retrieving distances from 15 nearest neighbours
Checking valid object
Constructing kNN graph with k:15
Retrieving distances from 15 nearest neighbours
Checking valid object
Constructing kNN graph with k:15
Retrieving distances from 15 nearest neighbours
Checking valid object
Constructing kNN graph with k:15
Retrieving distances from 15 nearest neighbours
Checking valid object

Old code

Robustness of test outcomes

I want to check whether using refined sampling allows to have more logFC even with different sampling

mean_nh_sizes
  [1]  28.64286  33.20732  35.25000  38.78472  38.53125  49.94444  62.41176  64.72549  74.22481  74.93976  63.75862  83.53968  90.72917
 [14] 100.84348 106.32168  75.80000 100.18644 115.23077 124.29630 131.64085  84.44000 116.83051 136.33333 149.01010 160.13846  27.04762
 [27]  30.27891  34.40984  35.60887  35.00300  48.63158  57.05983  64.41975  65.90868  68.34657  65.31111  78.37963  89.54015  93.79104
 [40]  99.00800  76.78049  96.50000 112.80488 120.74011 125.87391  84.68293 112.50000 131.10000 143.05357 150.30097  26.48810  31.16949
 [53]  31.85039  32.59831  33.01573  46.52055  55.45946  60.25123  61.62791  64.67363  60.72414  75.69173  85.32955  88.09506  92.12575
 [66]  71.59259  94.60484 108.88387 112.68333 119.77517  80.38636 111.71287 128.65248 136.46635 144.13910  25.00980  28.42035  31.47619
 [79]  31.28369  32.08755  45.50562  52.29775  58.47490  59.66289  62.16317  60.88889  74.37931  84.51402  85.57413  90.45431  72.01471
 [92]  92.57812 107.51020 110.75746 115.67049  80.37288 106.41739 126.79651 133.06276 142.97603  26.21552  28.05512  31.16071  30.49407
[105]  31.15677  44.42105  52.66497  58.34082  56.54884  60.65984  59.76471  74.73171  83.37885  83.07163  87.92148  69.76000  92.60000
[118] 105.17647 108.30323 112.15365  79.95161 110.25210 126.00546 131.74170 138.67422
run_milo_sampling <- function(graph, meta.df, model, X_pca, seed=42, sample.vertices=0.1){
  set.seed(seed)
  random.vertices <- sample(V(graph), size=floor(sample.vertices*length(V(graph))))
  vertex.knn <- BiocNeighbors::findKNN(X=X_pca, k=21, subset=as.vector(random.vertices))
  refined.vertices <- V(graph)[sapply(1:nrow(vertex.knn$index), function(i) refine_vertex(vertex.knn, i, X_pca))]
  
  vertex.list <- sapply(1:length(random.vertices), FUN=function(X) neighbors(graph, v=random.vertices[X]))
  vertex.list.refined <- sapply(1:length(refined.vertices), FUN=function(X) neighbors(graph, v=refined.vertices[X]))
  
  count.matrix.random <- countCells(sim2.knn, meta.df, vertex.list = vertex.list, random.vertices = random.vertices, sample.column = "sample")
  count.matrix.refined <- countCells(sim2.knn, meta.df, vertex.list = vertex.list.refined, random.vertices = refined.vertices, sample.column = "sample")
    
  spFDR.random <- testQLF(graph, count.matrix.random, model)
  spFDR.refined <- testQLF(graph, count.matrix.refined, model)
  
  fdr.df.random <- data.frame(Vertex=as.integer(rownames(spFDR.random$res)), p=spFDR.random$res$PValue, adjp=spFDR.random$spFDR, adjp_fdr=spFDR.random$res$FDR, logFC=spFDR.random$res$logFC, Sig=spFDR.random$res$Sig)
  fdr.df.refined <- data.frame(Vertex=as.integer(rownames(spFDR.refined$res)), p=spFDR.refined$res$PValue, adjp=spFDR.refined$spFDR, logFC=spFDR.refined$res$logFC, adjp_fdr=spFDR.refined$res$FDR, Sig=spFDR.refined$res$Sig)

  return(list(random=fdr.df.random, refined=fdr.df.refined))
}

sample_perc5 <- map(2020:2025, ~ run_milo_sampling(data_5k_cells$graph, data_5k_cells$meta.df, data_5k_cells$model, data_5k_cells$X_pca, seed=.x, sample.vertices = 0.05))
sample_perc10 <- map(2020:2025, ~ run_milo_sampling(data_5k_cells$graph, data_5k_cells$meta.df, data_5k_cells$model, data_5k_cells$X_pca, seed=.x, sample.vertices = 0.1))
sample_perc15 <- map(2020:2025, ~ run_milo_sampling(data_5k_cells$graph, data_5k_cells$meta.df, data_5k_cells$model, data_5k_cells$X_pca, seed=.x, sample.vertices = 0.15))
sample_perc20 <- map(2020:2025, ~ run_milo_sampling(data_5k_cells$graph, data_5k_cells$meta.df, data_5k_cells$model, data_5k_cells$X_pca, seed=.x, sample.vertices = 0.2))


make_test_df <- function(sample_df){
  sample_df %>%
  imap( ~ bind_rows(.x[["refined"]] %>% dplyr::mutate(sampling="refined"),
                     .x[["random"]] %>% dplyr::mutate(sampling="random")) %>%
           dplyr::mutate(s=.y)) %>%
    purrr::reduce(bind_rows) %>%
    left_join(data_5k_cells$meta.df) %>%
    dplyr::mutate(group_id = factor(group_id, levels=paste0('M', 1:num_milestones))) %>%
    group_by(sampling, s, group_id) %>%
    summarise(mean_logFC=mean(logFC)) 
  }

map(list(perc5=sample_perc5, perc10=sample_perc10, perc15=sample_perc15, perc20=sample_perc20), ~ make_test_df(.x)) %>%
  imap( ~ dplyr::mutate(.x, perc=.y)) %>%
  purrr::reduce(bind_rows) %>%
  ggplot(aes(group_id, mean_logFC, color=perc)) +
  # geom_pointrange(stat = "summary",
  #   fun.min = min,
  #   fun.max = max,
  #   fun = mean) +
  geom_boxplot(varwidth = TRUE) +
  facet_grid(.~sampling) +
  scale_fill_gradient2()

No big differences TBH


Compositional effect

Do I get high/low FC where unexpected just because things are changing elsewhere?

data_2k_cells <- simulate_linear_traj(num_cells = 2000, num_milestones = 10, prob_start = 0.5, prob_end=0.95)
ggplot(data_2k_cells$meta.df, aes(UMAP1, UMAP2, color=condition)) + geom_point(size=0.2) +
  theme_clean() 
ggplot(data_2k_cells$meta.df, aes(UMAP1, UMAP2, color=group_id)) + geom_point(size=0.2) +
  theme_clean() +
  geom_text(data = . %>% group_by(group_id) %>% summarise(UMAP1=first(UMAP1), UMAP2=first(UMAP2)), aes(label=group_id), color="black")

graph <- data_2k_cells$graph
sample.vertices <- 0.1
meta.df <- data_2k_cells$meta.df
model <- data_2k_cells$model
X_pca <- data_2k_cells$X_pca

random.vertices <- sample(V(graph), size=floor(sample.vertices*length(V(graph))))
vertex.knn <- BiocNeighbors::findKNN(X=X_pca, k=21, subset=as.vector(random.vertices))
refined.vertices <- V(graph)[sapply(1:nrow(vertex.knn$index), function(i) refine_vertex(vertex.knn, i, X_pca))]

vertex.list <- sapply(1:length(random.vertices), FUN=function(X) neighbors(graph, v=random.vertices[X]))
vertex.list.refined <- sapply(1:length(refined.vertices), FUN=function(X) neighbors(graph, v=refined.vertices[X]))

count.matrix.random <- countCells(graph, meta.df, vertex.list = vertex.list, random.vertices = random.vertices, sample.column = "sample")
count.matrix.refined <- countCells(graph, meta.df, vertex.list = vertex.list.refined, random.vertices = refined.vertices, sample.column = "sample")
  
spFDR.random <- testQLF(graph, count.matrix.random, model)
spFDR.refined <- testQLF(graph, count.matrix.refined, model)

Refined sampling seems to be able to identify DA at both ends of the spectrum better

fdr.df.random <- data.frame(Vertex=as.integer(rownames(spFDR.random$res)), p=spFDR.random$res$PValue, adjp=spFDR.random$spFDR, adjp_fdr=spFDR.random$res$FDR, logFC=spFDR.random$res$logFC, Sig=spFDR.random$res$Sig)
fdr.df.refined <- data.frame(Vertex=as.integer(rownames(spFDR.refined$res)), p=spFDR.refined$res$PValue, adjp=spFDR.refined$spFDR, logFC=spFDR.refined$res$logFC, adjp_fdr=spFDR.refined$res$FDR, Sig=spFDR.refined$res$Sig)

meta.df %>%
  left_join(fdr.df.random) %>%
  # dplyr::arrange(sampled) %>%
  ggplot(aes(UMAP1, UMAP2, 
             # color= - log10(adjp),
            # color= - log10(p),
             color = logFC
             )) +
  geom_point(size=0.5) +
  geom_point(data=. %>% dplyr::filter(!is.na(adjp))) +
  theme_clean() +
  scale_color_gradient2(midpoint = 0, high = "red", low="blue",na.value ="grey80") +
  ggtitle("Random sampling")


meta.df %>%
  left_join(fdr.df.refined) %>%
  # dplyr::arrange(sampled) %>%
  ggplot(aes(UMAP1, UMAP2, 
             # color= - log10(adjp),
            # color= - log10(p),
             color = logFC
             )) +
  geom_point(size=0.5) +
  geom_point(data=. %>% dplyr::filter(!is.na(adjp))) +
  theme_clean() +
  scale_color_gradient2(midpoint = 0, high = "red", low="blue",na.value ="grey80") +
  ggtitle("Refined sampling")
meta.df %>%
   left_join(fdr.df.refined) %>%
  dplyr::filter(!is.na(logFC)) %>%
  ggplot(aes(logFC, -log10(adjp), shape=Sig, color=group_id)) +
  geom_point() +
  ggtitle("refined sampling") +
meta.df %>%
   left_join(fdr.df.random) %>%
  dplyr::filter(!is.na(logFC)) %>%
  ggplot(aes(logFC, -log10(adjp), shape=Sig, color=group_id)) +
  geom_point() +
  ggtitle("random sampling") 
num_milestones=10

meta.df %>%
  ungroup() %>%
  left_join(fdr.df.refined) %>%
  dplyr::mutate(group_id = factor(group_id, levels=paste0('M', 1:num_milestones))) %>%
  dplyr::filter(!is.na(logFC)) %>%
  ggplot(aes(group_id, logFC,  shape=Sig, color=group_id, size= -log10(adjp))) +
  geom_jitter() +
  ggtitle("refined sampling") +
meta.df %>%
  ungroup() %>%
  left_join(fdr.df.random) %>%
  dplyr::mutate(group_id = factor(group_id, levels=paste0('M', 1:num_milestones))) %>%  dplyr::filter(!is.na(logFC)) %>%
  ggplot(aes(group_id, logFC, shape=Sig, color=group_id, size= -log10(adjp))) +
  geom_jitter() +
  ggtitle("random sampling") 

How many neighborhoods disappear w refinement?

simulate_linear_traj <- function(num_cells, num_milestones, num_features=1000, k_param=21, seed=42,
                                 prob_start=0.1, prob_end=0.9){
  set.seed(seed)
  ## Generate simulated dataset of trajectory
  dataset <- generate_dataset(
    model = model_linear(num_milestones = num_milestones),
    num_cells = num_cells,
    num_features = num_features
  )
  sim2.gex <- as.matrix(dataset$expression)
  sim2.branches <- dataset$prior_information$groups_id
  sim2.time = dataset$prior_information$timecourse_continuous
  
  ## Build graph 
  sim2.pca <- prcomp_irlba(sim2.gex, n=50, scale.=TRUE, center=TRUE)
  X_pca = sim2.pca$x[, c(1:30)]
  sim2.knn <- buildKNNGraph(x=X_pca, k=k_param, d=NA, transposed=TRUE)
  ## Run UMAP
  stem.ta.umap <- umap(sim2.pca$x[, c(1:30)],
                       n_components=2,
                       n_neighbors=k_param, metric='euclidean',
                       init='random', min_dist=0.1)
  dyn.df <- data.frame(UMAP1=stem.ta.umap$layout[,1], UMAP2=stem.ta.umap$layout[,2], 
             cell_id=rownames(sim2.gex), time=sim2.time)
  dyn.df <- dyn.df %>% left_join(sim2.branches)
  
  ## Simulate conditions
  n_groups <- length(unique(dyn.df$group_id))
  p_vec <- seq(prob_start, prob_end, length.out = n_groups)
  a.cells <- c()
  for (i in 1:n_groups) {
    g <- paste0("M",i)
    p <- p_vec[i] 
    m.A <- sample(dyn.df$cell_id[dyn.df$group_id==g], 
                  size=floor(sum(dyn.df$group_id==g)*p))
    a.cells <- c(a.cells, m.A)
  }
  
  dyn.df <- dyn.df %>% dplyr::mutate(condition = ifelse(cell_id %in% a.cells, "A", 'B')) 

  ## Simulate replicates
  dyn.df <- dyn.df %>%
    group_by(group_id) %>%
    dplyr::mutate(replicate=c(rep("R1", floor(n()*0.3)), 
                              rep("R2", floor(n()*0.3)), 
                              rep("R3", n() - 2*(floor(n()*0.3))))
    ) 
  
  ## Add sample name (condition + replicate)
  dyn.df$sample <- paste(dyn.df$condition, dyn.df$replicate, sep="_")
  ## Add vertex id (for counts)
  dyn.df$Vertex <- as.vector(V(sim2.knn))
  
  ## Make model matrix for testing
  sample.meta <- data.frame("Condition"=c(rep("A", 3), rep("B", 3)),
                            "Replicate"=rep(c("R1", "R2", "R3"), 2))
  sample.meta$Sample <- paste(sample.meta$Condition, sample.meta$Replicate, sep="_")
  rownames(sample.meta) <- sample.meta$Sample
  sim2.model <- model.matrix(~ 0 + Condition, data=sample.meta)
  
  return(list(graph=sim2.knn,
              X_pca=X_pca,
              meta.df=dyn.df,
              model=sim2.model))
  
  }
data_2k_cells <- simulate_linear_traj(num_cells = 2000, num_milestones = 10, prob_start = 0.5, prob_end=0.95)

graph <- data_2k_cells$graph
sample.vertices <- 0.1
meta.df <- data_2k_cells$meta.df
model <- data_2k_cells$model
X_pca <- data_2k_cells$X_pca

random.vertices <- sample(V(graph), size=floor(sample.vertices*length(V(graph))))
vertex.knn <- BiocNeighbors::findKNN(X=X_pca, k=21, subset=as.vector(random.vertices))
refined.vertices <- V(graph)[sapply(1:nrow(vertex.knn$index), function(i) refine_vertex(vertex.knn, i, X_pca))]


data.frame(random=as.numeric(random.vertices), refined=as.numeric(refined.vertices)) %>%
  rowid_to_column() %>%
  group_by(as.factor(refined)) %>%
  dplyr::mutate(n_converging = n()) %>%
  ungroup() %>%
  pivot_longer(cols=c('random', "refined"), names_to = "sampling_scheme", values_to = "Vertex") %>%
  left_join(meta.df, by="Vertex") %>%
  ggplot(aes(time,sampling_scheme, color=n_converging)) +
  geom_point(size=0.5) +
  geom_line(aes(group=rowid), size=0.5) +
  scale_color_viridis_c()
data.frame(random=as.numeric(random.vertices), refined=as.numeric(refined.vertices)) %>%
  rowid_to_column() %>%
  group_by(as.factor(refined)) %>%
  dplyr::mutate(n_converging = n()) %>%
  ggplot(aes(as.factor(n_converging))) + geom_histogram(stat="count")

After refinement what is the distance to the nearest sampled cell?

Distances should become more uniform

get_dist_to_closest_neigh <- function(graph, sample.vertices){
  random.vertices <- sample(V(graph), size=floor(sample.vertices*length(V(graph))))
  vertex.knn <- BiocNeighbors::findKNN(X=X_pca, k=21, subset=as.vector(random.vertices))
  refined.vertices <- V(graph)[sapply(1:nrow(vertex.knn$index), function(i) refine_vertex(vertex.knn, i, X_pca))]
  dist_to_closest_random <- BiocNeighbors::findKNN(X=X_pca[as.vector(random.vertices),], k=1)[["distance"]]
  dist_to_closest_refined <- BiocNeighbors::findKNN(X=X_pca[unique(as.vector(refined.vertices)),], k=1)[["distance"]]
  dist_df <- bind_rows(data.frame(distance_to_closest=dist_to_closest_refined, sampling_scheme='refined'),
          data.frame(distance_to_closest=dist_to_closest_random, sampling_scheme='random')) %>%
    dplyr::mutate(sample_perc=sample.vertices)
}

dist_ls <- map(seq(0.1,0.6, by = 0.05), ~ get_dist_to_closest_neigh(graph, .x)) 
purrr::reduce(dist_ls, bind_rows) %>%
  ggplot(aes(as.factor(sample_perc), distance_to_closest, color=sampling_scheme)) +
  # ggbeeswarm::geom_quasirandom()
  geom_boxplot(varwidth = TRUE) +
  xlab("% sampled") + ylab("Distance to closest sample") +
  theme_grey(base_size = 14)

What is the relationship between neighborhood size and k?

data_5k_cells <- simulate_linear_traj(num_cells = 5000, num_milestones = 10)
meta.df <- data_5k_cells$meta.df
model <- data_5k_cells$model
X_pca <- data_5k_cells$X_pca

k_vec <- seq(10,50, by=5)
graph_ls <- map(k_vec, ~ buildKNNGraph(x=X_pca, k=.x, d=NA, transposed=TRUE))
get_neigh_df <- function(sampled_vertices, graph, X_pca, k_param, sampling_mode="random"){
  if (sampling_mode=="refined") {
    vertex.knn <- BiocNeighbors::findKNN(X=X_pca, k=k_param, subset=as.vector(sampled_vertices))
    sampled_vertices <- V(graph)[sapply(1:nrow(vertex.knn$index), function(i) refine_vertex(vertex.knn, i, X_pca))]
  }
  sampled_vertices <- unique(sampled_vertices)
  vertex.list <- sapply(1:length(sampled_vertices), FUN=function(X) neighbors(graph, v=sampled_vertices[X]))
  neigh_df <- data.frame(neigh_vertex=as.vector(sampled_vertices), neigh_size=sapply(vertex.list, function(x) length(x)), 
                         sampling_mode=sampling_mode, k=k_param)
  return(neigh_df)
  }

neigh_df_ls <- lapply(seq_along(k_vec), function(i){
  random_sample <- sample(V(graph_ls[[i]]), size=floor(sample.vertices*length(V(graph_ls[[i]]))))
  sampled_vertices <- random_sample
  random_neigh_df <- get_neigh_df(random_sample, graph_ls[[i]], X_pca, k_vec[i], sampling_mode="random")
  refined_neigh_df <- get_neigh_df(random_sample, graph_ls[[i]], X_pca, k_vec[i], sampling_mode="refined")
  bind_rows(random_neigh_df, refined_neigh_df)
  })

purrr::reduce(neigh_df_ls, bind_rows) %>%
  ggplot(aes(as.factor(k), neigh_size, color=sampling_mode)) +
  geom_violin(scale = "width") +
  geom_boxplot(width=0.2) +
  facet_wrap(sampling_mode~.) +
  xlab("K") + ylab("Neighborhood size") +
  theme_clean(base_size = 18)
purrr::reduce(neigh_df_ls, bind_rows) %>%
  ggplot(aes(as.factor(k), neigh_size / k, color=sampling_mode)) +
  geom_violin(scale = "width") +
  geom_boxplot(width=0.2) +
  facet_wrap(sampling_mode~.) +
  xlab("K") + ylab("Neighborhood size / K") +
  theme_clean(base_size = 18)

Relationship between K and neighborhood size

purrr::reduce(neigh_df_ls, bind_rows) %>%
  group_by(sampling_mode, k) %>%
  summarise(n=n()) %>%
  ggplot(aes(k,n, color=sampling_mode)) +
  geom_point()
LS0tCnRpdGxlOiAiTWlsbyAtIHNpbXVsYXRlZCB0cmFqZWN0b3J5IGdyYXBocyIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9CmxpYnJhcnkoZ2dwbG90MikKbGlicmFyeShTaW5nbGVDZWxsRXhwZXJpbWVudCkKbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkoaWdyYXBoKQpsaWJyYXJ5KHNjcmFuKQoKZGV2dG9vbHM6OmxvYWRfYWxsKCJ+L21pbG8vbWlsb1IvIikKCiMgZGV2dG9vbHM6Omluc3RhbGxfZ2l0aHViKCJkeW52ZXJzZS9keW50b3kiKQpsaWJyYXJ5KGR5bnRveSkKYGBgCgojIyBUZXN0aW5nIHdpdGggcmFuZG9tIHNhbXBsaW5nCgojIyMgU2ltdWxhdGUgbGluZWFyIHRyYWplY3RvcnkKClVzaW5nIFtgZHludG95YF0oaHR0cHM6Ly9naXRodWIuY29tL2R5bnZlcnNlL2R5bnRveSksIEkgc2ltdWxhdGUgYSBzY1JOQS1zZXEgZGF0YSB3aXRoIGNlbGxzIGZvcm1pbmcgYSBsaW5lYXIgdHJhamVjdG9yeSAobm8gYnJhbmNoZXMpIHdpdGggNTAwMCBjZWxscyBhbmQgMTAgbWFpbiBzdGF0ZXMgKG1pbGVzdG9uZXMpLgoKYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9CnNldC5zZWVkKDQyKQpkYXRhc2V0IDwtIGdlbmVyYXRlX2RhdGFzZXQoCiAgbW9kZWwgPSBtb2RlbF9saW5lYXIobnVtX21pbGVzdG9uZXMgPSAxMCksCiAgbnVtX2NlbGxzID0gNTAwMCwKICBudW1fZmVhdHVyZXMgPSA1MDAwCikKCmdleCA8LSBhcy5tYXRyaXgoZGF0YXNldCRleHByZXNzaW9uKQpicmFuY2hlcyA8LSBkYXRhc2V0JHByaW9yX2luZm9ybWF0aW9uJGdyb3Vwc19pZAogIAojIyBEaW1lbnNpb25hbGl0eSByZWR1Y3Rpb24KcGNhIDwtIHByY29tcF9pcmxiYShnZXgsIG49NTAsIHNjYWxlLj1UUlVFLCBjZW50ZXI9VFJVRSkKWF9wY2EgPSBwY2EkeFssIGMoMTozMCldCmBgYAoKSSBhc3NpZ24gY2VsbHMgdG8gc2ltdWxhdGVkIGJpb2xvZ2ljYWwgY29uZGl0aW9ucyBhbmQgcmVwbGljYXRlcywgdGhhdCB3ZSB3aWxsIHVzZSBmb3IgZGlmZmVyZW50aWFsIGFidW5kYW5jZSB0ZXN0aW5nLiBGb3IgZWFjaCBvZiB0aGUgJE0kIGNsdXN0ZXJzLCBJIGFzc2lnbiBkaWZmZXJlbnQgcHJvcG9ydGlvbnMgb2YgY2VsbHMgdG8gY29uZGl0aW9uIEEgb3IgY29uZGl0aW9uIEIsIHdoaWxlIHNpbXVsYXRpbmcgcHJvcG9ydGlvbmF0ZSBtaXhpbmcgYmV0d2VlbiByZXBsaWNhdGVzLgoKYGBge3J9CmNvbGRhdGFfZGYgPC0gZGF0YS5mcmFtZShjZWxsX2lkID0gcm93bmFtZXMoZ2V4KSkKY29sZGF0YV9kZiA8LSBsZWZ0X2pvaW4oY29sZGF0YV9kZiwgYnJhbmNoZXMpCiAgCiMjIFNpbXVsYXRlIGJpb2xvZ2ljYWwgY29uZGl0aW9uCnByb2Jfc3RhcnQgPC0gMC4wNQpwcm9iX2VuZCA8LSAwLjk1Cm5fZ3JvdXBzIDwtIGxlbmd0aCh1bmlxdWUoYnJhbmNoZXMkZ3JvdXBfaWQpKQpwX3ZlYyA8LSBzZXEocHJvYl9zdGFydCwgcHJvYl9lbmQsIGxlbmd0aC5vdXQgPSBuX2dyb3VwcykKYS5jZWxscyA8LSBjKCkKZm9yIChpIGluIDE6bl9ncm91cHMpIHsKICBnIDwtIHBhc3RlMCgiTSIsaSkKICBwIDwtIHBfdmVjW2ldIAogIG0uQSA8LSBzYW1wbGUoY29sZGF0YV9kZiRjZWxsX2lkW2NvbGRhdGFfZGYkZ3JvdXBfaWQ9PWddLCAKICAgICAgICAgICAgICAgIHNpemU9Zmxvb3Ioc3VtKGNvbGRhdGFfZGYkZ3JvdXBfaWQ9PWcpKnApKQogIGEuY2VsbHMgPC0gYyhhLmNlbGxzLCBtLkEpCn0KCmNvbGRhdGFfZGYgPC0gY29sZGF0YV9kZiAlPiUgZHBseXI6Om11dGF0ZShjb25kaXRpb24gPSBpZmVsc2UoY2VsbF9pZCAlaW4lIGEuY2VsbHMsICJBIiwgJ0InKSkgCgojIyBTaW11bGF0ZSByZXBsaWNhdGVzCmNvbGRhdGFfZGYgPC0gY29sZGF0YV9kZiAlPiUKICBncm91cF9ieShncm91cF9pZCkgJT4lCiAgZHBseXI6Om11dGF0ZShyZXBsaWNhdGU9YyhyZXAoIlIxIiwgZmxvb3IobigpKjAuMykpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcCgiUjIiLCBmbG9vcihuKCkqMC4zKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwKCJSMyIsIG4oKSAtIDIqKGZsb29yKG4oKSowLjMpKSkpCiAgKSAKCiMjIEFkZCBzYW1wbGUgbmFtZSAoY29uZGl0aW9uICsgcmVwbGljYXRlKQpjb2xkYXRhX2RmJHNhbXBsZSA8LSBwYXN0ZShjb2xkYXRhX2RmJGNvbmRpdGlvbiwgY29sZGF0YV9kZiRyZXBsaWNhdGUsIHNlcD0iXyIpCgpoZWFkKGNvbGRhdGFfZGYpCmBgYAoKQ29uc3RydWN0IGEgYE1pbG9gIG9iamVjdAoKYGBge3J9CiMjIE1ha2UgU2luZ2xlQ2VsbEV4cGVyaW1lbnQgb2JqZWN0CmNvbGRhdGEgPC0gY29sZGF0YV9kZiAlPiUgY29sdW1uX3RvX3Jvd25hbWVzKCJjZWxsX2lkIikKc2ltX3NjZSA8LSBTaW5nbGVDZWxsRXhwZXJpbWVudChhc3NheT1saXN0KGNvdW50cz10KGdleCkpLCBjb2xEYXRhPWNvbGRhdGEpCmxvZ2NvdW50cyhzaW1fc2NlKSA8LSBsb2cyKGNvdW50cyhzaW1fc2NlKSArIDEpCnJlZHVjZWREaW1zKHNpbV9zY2UpIDwtIFNpbXBsZUxpc3QoUENBID0gWF9wY2EpCgojIyBNYWtlIG1pbG8gb2JqZWN0CnNpbV9taWxvIDwtIE1pbG8oc2ltX3NjZSkKc2ltX21pbG8KYGBgCgojIyMgQnVpbGQgS05OLWdyYXBoCgpgYGB7cn0Kc2ltX21pbG8gPC0gYnVpbGRHcmFwaChzaW1fbWlsbywgayA9IDIwLCBkID0gMzApCmBgYAoKIyMjIE1ha2UgbmVpZ2hib3VyaG9vZHMKClVzaW5nIHNhbXBsaW5nIG9mIDEwJSByYW5kb20gcG9pbnRzIAoKYGBge3J9CnNpbV9taWxvIDwtIG1ha2VOZWlnaGJvdXJob29kcyhzaW1fbWlsbywgcHJvcCA9IDAuMSwgaz0yMCwgZD0zMCwgcmVmaW5lZCA9IEZBTFNFKQpwbG90TmVpZ2hib3Job29kU2l6ZUhpc3Qoc2ltX21pbG8pCmBgYAoKIyMjIENvdW50IGNlbGxzIHdpdGhpbiBuZWlnaGJvdXJob29kcwpgYGB7cn0Kc2ltX21pbG8gPC0gY291bnRDZWxscyhzaW1fbWlsbywgZGF0YSA9IGRhdGEuZnJhbWUoY29sRGF0YShzaW1fbWlsbykpLCBzYW1wbGVzID0gInNhbXBsZSIpCmhlYWQobmVpZ2hib3VyaG9vZENvdW50cyhzaW1fbWlsbykpCmBgYAoKIyMjIFRlc3QgZm9yIERBCgpgYGB7cn0KIyMgQnVpbGQgZGVzaWduIG1hdHJpeApkZXNpZ25fZGYgPC0gZGF0YS5mcmFtZShjb2xEYXRhKHNpbV9taWxvKSkgJT4lCiAgcm93bmFtZXNfdG9fY29sdW1uKCkgJT4lCiAgc2VsZWN0KHNhbXBsZSwgY29uZGl0aW9uKSAlPiUKICBkaXN0aW5jdCgpIAoKbWlsb19yZXN1bHRzIDwtIHRlc3ROZWlnaGJvdXJob29kcyhzaW1fbWlsbywgfiAxICsgY29uZGl0aW9uLCBkYXRhID0gZGVzaWduX2RmLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZmRyLndlaWdodGluZyA9ICJrLWRpc3RhbmNlIikKCmhlYWQobWlsb19yZXN1bHRzKQpgYGAKCmBgYHtyfQptaWxvX3Jlc3VsdHMgJT4lCiAgbXV0YXRlKGlzX3NpZ25pZj1pZmVsc2UoU3BhdGlhbEZEUjwgMC4wNSwgMSwwKSkgJT4lCiAgZ2dwbG90KGFlcyhsb2dGQywgLWxvZzEwKFNwYXRpYWxGRFIpLCBjb2xvcj1pc19zaWduaWYpKSArIAogIGdlb21fcG9pbnQoKSArCiAgc2NhbGVfY29sb3JfZ3JhZGllbnQoaGlnaD0icmVkIikKYGBgCgpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZWNobz1UUlVFfQpuaEluZGV4IDwtIHVubGlzdChuZWlnaGJvdXJob29kSW5kZXgoc2ltX21pbG8pKQpuaFNpemUgPC0gc2FwcGx5KG5laWdoYm91cmhvb2RzKHNpbV9taWxvKSwgbGVuZ3RoKQptaWxvX3Jlc3VsdHMgPC0gbXV0YXRlKG1pbG9fcmVzdWx0cywgCiAgICAgICAgICAgICAgICAgICAgICAgbmhJbmRleD1uaEluZGV4LCBuaFNpemU9bmhTaXplKQoKZGYgPC0gY29sRGF0YShzaW1fbWlsbykgJT4lCiAgZGF0YS5mcmFtZSgpICU+JQogIHJvd2lkX3RvX2NvbHVtbigibmhJbmRleCIpICU+JQogIGxlZnRfam9pbihtaWxvX3Jlc3VsdHMpICU+JQogIG11dGF0ZShncm91cF9pZD1mYWN0b3IoZ3JvdXBfaWQsIGxldmVscz1zdHJfYygiTSIsIDE6MTApKSkKCmRmICU+JQogIGdncGxvdChhZXMoZ3JvdXBfaWQsIGxvZ0ZDKSkgKyAKICBnZW9tX2ppdHRlcihhZXMoY29sb3I9LWxvZzEwKFNwYXRpYWxGRFIpLCBzaXplPW5oU2l6ZSksIGFscGhhPTAuNikgKwogIHNjYWxlX2NvbG9yX3ZpcmlkaXNfYygpCmBgYAoKVmlzdWFsaXplIHJlc3VsdHMgaW4gZW1iZWRkaW5nCgpgYGB7cn0KIyMgQ29tcHV0ZSBVTUFQIGZvciBhbGwgY2VsbHMgCnNpbV91bWFwIDwtIHV3b3Q6OnVtYXAocmVkdWNlZERpbShzaW1fbWlsbyksIG5fbmVpZ2hib3JzID0gMjAsIG1ldHJpYyA9ICJjb3NpbmUiKQpyZWR1Y2VkRGltKHNpbV9taWxvLCAiVU1BUCIpIDwtIHNpbV91bWFwCgojIyBUYWtlIG1lZGlhbiBvZiBuZWlnaGJvdXJob29kcwpuaE1lZCA8LSB0KHNhcHBseShuZWlnaGJvdXJob29kcyhzaW1fbWlsbyksIGZ1bmN0aW9uKHgpIGNvbE1lZGlhbnMocmVkdWNlZERpbShzaW1fbWlsbylbeCxdKSkpCnVtYXBfbmhNZWQgPC0gdXdvdDo6dW1hcChuZWlnaGJvdXJob29kc01lZCwgbl9uZWlnaGJvcnM9MjAsIG1ldHJpYz0nY29zaW5lJywKICAgICAgICAgICBpbml0PXJlZHVjZWREaW0oc2ltX21pbG9bLHVubGlzdChuZWlnaGJvdXJob29kSW5kZXgoc2ltX21pbG8pKV0sICJVTUFQIiksIG1pbl9kaXN0PTAuMSkKY29sbmFtZXModW1hcF9uaE1lZCkgPC0gYygiVU1BUF8xIiwgIlVNQVBfMiIpCgptaWxvX3Jlc3VsdHNfdml6IDwtIGNiaW5kKG1pbG9fcmVzdWx0cywgdW1hcF9uaE1lZCkgCm1pbG9fcmVzdWx0c192aXogJT4lCiAgYXJyYW5nZShhYnMobG9nRkMpKSAlPiUKICBtdXRhdGUobG9nRkMgPSBpZmVsc2UoU3BhdGlhbEZEUiA+IGZpbHRlcl9hbHBoYSwgTkEsIGxvZ0ZDKSkgJT4lCiAgZ2dwbG90KGFlcyhVTUFQXzEsIFVNQVBfMikpICsKICBnZW9tX3BvaW50KGFlcyhjb2xvcj1sb2dGQykpICsKICBzY2FsZV9jb2xvcl9ncmFkaWVudDIobWlkcG9pbnQgPSAwLCBoaWdoPSJyZWQiLCBsb3c9ImJsdWUiLCBuYW1lPSJsb2ctRkMiKSArCiAgdGhlbWVfY2xhc3NpYyhiYXNlX3NpemUgPSAxNikgKwogIHRoZW1lKGF4aXMudGlja3MgPWVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0ID0gZWxlbWVudF9ibGFuaygpLCBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0PTAuNSkpCmBgYAoKYGBge3J9CnBsb3RNaWxvUmVkdWNlZERpbShzaW1fbWlsbywgbWlsb19yZXN1bHRzLCByZWR1Y2VkX2RpbSA9ICJVTUFQIiwgY29tcG9uZW50cyA9IGMoMSwyKSwgZmlsdGVyX2FscGhhID0gMC4xKQpgYGAKCiMjIFJlZmluZWQgc2FtcGxpbmcgc2NoZW1lCgpXZSBhZG9wdCB0aGUgcmVmaW5lZCBzYW1wbGluZyBzdHJhdGVneSBhcHBsaWVkIGluIFtXaXNoYm9uZV0oaHR0cHM6Ly93d3cubmF0dXJlLmNvbS9hcnRpY2xlcy9uYnQuMzU2OSNTZWMxMiksIGFuZCBhZGFwdGVkIGZyb20gW2hlcmVdKGh0dHBzOi8vd3d3Lm5hdHVyZS5jb20vYXJ0aWNsZXMvbm1ldGguMzU0NSkuIEJyaWVmbHksIHRvIGF2b2lkIHNlbGVjdGluZyBvdXRsaWVycyB3aXRoIHJhbmRvbSBzYW1wbGluZywgSSBmaXJzdCByYW5kb21seSBzZWxlY3QgJG4kIGNlbGxzLiBGb3IgZWFjaCBzYW1wbGVkIGNlbGwgSSB0aGVuIGlkZW50aWZ5IGl0cyBrIG5lYXJlcyBuZWlnaGJvcnMgYW5kIGNvbXB1dGUgdGhlIG1lZGlhbiBwcm9maWxlIG9mIHRoZSBuZWlnaGJvcnMgKGluIHRoaXMgY2FzZSB0aGUgcHJvZmlsZSBpbiByZWR1Y2VkIFBDIHNwYWNlKS4gVGhlbiBJIHJlcGxhY2UgZWFjaCBzYW1wbGVkIGNlbGwgYnkgdGhlIGNlbGwgY2xvc2VzdCB0byB0aGUgbWVkaWFuIHByb2ZpbGUgb2YgaXRzIG5laWdoYm9ycy4gCgpgYGB7cn0Kc2ltX21pbG9fcmVmIDwtIG1ha2VOZWlnaGJvdXJob29kcyhzaW1fbWlsbyxwcm9wID0gMC4xLCBrPTIwLCBkPTMwLCByZWZpbmVkID0gVFJVRSkKYGBgCgojIyMgUmVmaW5lZCBzYW1wbGluZyBzdGF0cwoKV2l0aCB0aGUgcmVmaW5lZCBzYW1wbGluZyBzY2hlbWUgSSBzZWxlY3QgY2VsbHMgd2l0aCBhIGxhcmdlciBuZWlnaGJvdXJob29kIG9uIGF2ZXJhZ2UuCgpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZWNobz1UUlVFfQpiaW5kX3Jvd3MoCiAgZGF0YS5mcmFtZShuZWlnaGJvcmhvb2Rfc2l6ZSA9IHVubGlzdChsYXBwbHkobmVpZ2hib3VyaG9vZHMoc2ltX21pbG8pLCBsZW5ndGgpKSwgc2FtcGxpbmc9InJhbmRvbSIpLAogIGRhdGEuZnJhbWUobmVpZ2hib3Job29kX3NpemUgPSB1bmxpc3QobGFwcGx5KG5laWdoYm91cmhvb2RzKHNpbV9taWxvX3JlZiksIGxlbmd0aCkpLCBzYW1wbGluZz0icmVmaW5lZCIpCiAgKSAlPiUKICBnZ3Bsb3QoYWVzKG5laWdoYm9yaG9vZF9zaXplLCBmaWxsPXNhbXBsaW5nKSkgKwogIGdlb21faGlzdG9ncmFtKGJpbnM9MzApICsKICBmYWNldF9ncmlkKHNhbXBsaW5nfi4sIHNjYWxlcz0iZnJlZV95IikgKwogIHRoZW1lX2dyZXkoYmFzZV9zaXplPTE1KQpgYGAKCldoZW4gJG4kIGlzIGxhcmdlIEkgb2Z0ZW4gZW5kIHVwIHNhbXBsaW5nIGxlc3MgdGhhbiAkbiQgY2VsbHMgYmVjYXVzZSBmb3IgbWFueSByYW5kb21seSBzYW1wbGVkIGNlbGxzIHRoZSBjZWxsIGNsb3Nlc3QgdG8gdGhlIEtOTnMgaXMgdGhlIHNhbWUuIAoKYGBge3J9CnJhbmRvbV9uIDwtIGMoKQpyZWZpbmVkX24gPC0gYygpCmZvciAoeCBpbiBzZXEoMC4wMSwwLjUsIGJ5ID0gMC4wNSkpIHsKICBmb3IgKGkgaW4gMTozKXsKICAgIHJhbmRvbV9uaCAgPC0gbmVpZ2hib3VyaG9vZHMobWFrZU5laWdoYm91cmhvb2RzKHNpbV9taWxvLCBwcm9wPXgsIGs9MjAsIGQ9MzAsIHJlZmluZWQgPSBGQUxTRSwgc2VlZD00MiArIGkpKQogICAgcmVmaW5lZF9uaCA8LSBuZWlnaGJvdXJob29kcyhtYWtlTmVpZ2hib3VyaG9vZHMoc2ltX21pbG8sIHByb3A9eCwgaz0yMCwgZD0zMCwgcmVmaW5lZCA9IFRSVUUsIHNlZWQ9NDIgKyBpKSkKICAgIGxfcmFuIDwtIGxlbmd0aChyYW5kb21fbmgpCiAgICBsX3JlZiA8LSBsZW5ndGgocmVmaW5lZF9uaCkKICAgIHJhbmRvbV9uIDwtIGMocmFuZG9tX24sIGxfcmFuKQogICAgcmVmaW5lZF9uIDwtIGMocmVmaW5lZF9uLCBsX3JlZikKICAgIH0KfQoKZGF0YS5mcmFtZShzYW1wbGVfcHJvcG9ydGlvbj1sYXBwbHkoc2VxKDAuMDEsMC41LCBieSA9IDAuMDUpLCBmdW5jdGlvbih4KSByZXAoeCwgMykpICU+JSBwdXJycjo6cmVkdWNlKGMpLCByYW5kb20gPSByYW5kb21fbiwgcmVmaW5lZCA9IHJlZmluZWRfbikgJT4lCiAgcGl2b3RfbG9uZ2VyKGNvbHMgPSAtIHNhbXBsZV9wcm9wb3J0aW9uLCBuYW1lc190byA9ICdzYW1wbGluZycsIHZhbHVlc190byA9ICJuIikgJT4lCiAgZ2dwbG90KGFlcyhzYW1wbGVfcHJvcG9ydGlvbiwgbiwgY29sb3I9c2FtcGxpbmcpKSArIAogIGdlb21fcG9pbnQoc2l6ZT0xKSArCiAgdGhlbWVfYncoYmFzZV9zaXplID0gMTYpCmBgYAoKIyMjIENvbXBhcmUgREEgdGVzdGluZyByZXN1bHRzCgpgYGB7cn0Kc2ltX21pbG9fcmVmIDwtIGNvdW50Q2VsbHMoc2ltX21pbG9fcmVmLCBkYXRhID0gZGF0YS5mcmFtZShjb2xEYXRhKHNpbV9taWxvX3JlZikpLCBzYW1wbGVzID0gInNhbXBsZSIpCgpyZXNfcmFuZCA8LSB0ZXN0TmVpZ2hib3VyaG9vZHMoc2ltX21pbG8sIH4gMSArIGNvbmRpdGlvbiwgZGF0YSA9IGRlc2lnbl9kZiwgZmRyLndlaWdodGluZyA9ICJrLWRpc3RhbmNlIikKcmVzX3JlZiA8LSB0ZXN0TmVpZ2hib3VyaG9vZHMoc2ltX21pbG9fcmVmLCB+IDEgKyBjb25kaXRpb24sIGRhdGEgPSBkZXNpZ25fZGYsIGZkci53ZWlnaHRpbmcgPSAiay1kaXN0YW5jZSIpCmBgYAoKUmVmaW5lZCBzYW1wbGluZyBzZWVtcyB0byBiZSBhYmxlIHRvIGlkZW50aWZ5IERBIGF0IGJvdGggZW5kcyBvZiB0aGUgc3BlY3RydW0gYmV0dGVyCgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KcGxvdE1pbG9SZWR1Y2VkRGltKHNpbV9taWxvLCByZXNfcmFuZCwgcHRfc2l6ZT0yKSArIGdndGl0bGUoIlJhbmRvbSBzYW1wbGluZyIpIApwbG90TWlsb1JlZHVjZWREaW0oc2ltX21pbG9fcmVmLCByZXNfcmVmLCBwdF9zaXplPTIpICsgZ2d0aXRsZSgiUmVmaW5lZCBzYW1wbGluZyIpCmBgYAoKPCEtLSBgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRSwgZmlnLndpZHRoPTEwLCBmaWcuaGVpZ2h0PTV9IC0tPgo8IS0tIGR5bi5kZiAlPiUgLS0+CjwhLS0gICAgbGVmdF9qb2luKGZkci5kZi5yZWZpbmVkKSAlPiUgLS0+CjwhLS0gICBkcGx5cjo6ZmlsdGVyKCFpcy5uYShsb2dGQykpICU+JSAtLT4KPCEtLSAgIGdncGxvdChhZXMobG9nRkMsIC1sb2cxMChhZGpwKSwgc2hhcGU9U2lnLCBjb2xvcj1ncm91cF9pZCkpICsgLS0+CjwhLS0gICBnZW9tX3BvaW50KCkgKyAtLT4KPCEtLSAgIGdndGl0bGUoInJlZmluZWQgc2FtcGxpbmciKSArIC0tPgo8IS0tIGR5bi5kZiAlPiUgLS0+CjwhLS0gICAgbGVmdF9qb2luKGZkci5kZi5yYW5kb20pICU+JSAtLT4KPCEtLSAgIGRwbHlyOjpmaWx0ZXIoIWlzLm5hKGxvZ0ZDKSkgJT4lIC0tPgo8IS0tICAgZ2dwbG90KGFlcyhsb2dGQywgLWxvZzEwKGFkanApLCBzaGFwZT1TaWcsIGNvbG9yPWdyb3VwX2lkKSkgKyAtLT4KPCEtLSAgIGdlb21fcG9pbnQoKSArIC0tPgo8IS0tICAgZ2d0aXRsZSgicmFuZG9tIHNhbXBsaW5nIikgIC0tPgo8IS0tIGBgYCAtLT4KCgo8IS0tIGBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBmaWcud2lkdGg9MTAsIGZpZy5oZWlnaHQ9NX0gLS0+CjwhLS0gZHluLmRmICU+JSAtLT4KPCEtLSAgICBsZWZ0X2pvaW4oZmRyLmRmLnJlZmluZWQpICU+JSAtLT4KPCEtLSAgIGRwbHlyOjpmaWx0ZXIoIWlzLm5hKGxvZ0ZDKSkgJT4lIC0tPgo8IS0tICAgZ2dwbG90KGFlcyhncm91cF9pZCwgbG9nRkMsICBzaGFwZT1TaWcsIGNvbG9yPWdyb3VwX2lkLCBzaXplPSAtbG9nMTAoYWRqcCkpKSArIC0tPgo8IS0tICAgZ2VvbV9qaXR0ZXIoKSArIC0tPgo8IS0tICAgZ2d0aXRsZSgicmVmaW5lZCBzYW1wbGluZyIpICsgLS0+CjwhLS0gZHluLmRmICU+JSAtLT4KPCEtLSAgICBsZWZ0X2pvaW4oZmRyLmRmLnJhbmRvbSkgJT4lIC0tPgo8IS0tICAgZHBseXI6OmZpbHRlcighaXMubmEobG9nRkMpKSAlPiUgLS0+CjwhLS0gICBnZ3Bsb3QoYWVzKGdyb3VwX2lkLCBsb2dGQywgc2hhcGU9U2lnLCBjb2xvcj1ncm91cF9pZCwgc2l6ZT0gLWxvZzEwKGFkanApKSkgKyAtLT4KPCEtLSAgIGdlb21faml0dGVyKCkgKyAtLT4KPCEtLSAgIGdndGl0bGUoInJhbmRvbSBzYW1wbGluZyIpICAtLT4KPCEtLSBgYGAgLS0+CgpBcyBleHBlY3RlZCBtdWx0aXBsZSB0ZXN0aW5nIGNvcnJlY3Rpb24gaXMgbGVzcyBzZXZlcmUgd2l0aCB0aGUgcmVmaW5lZCBzYW1wbGUgc2V0IChsZXNzIHBvaW50cykKCmBgYHtyfQpyZXNfcmFuZCAlPiUKICBnZ3Bsb3QoYWVzKC1sb2cxMChQVmFsdWUpLCAtbG9nMTAoU3BhdGlhbEZEUikpKSArCiAgZ2VvbV9hYmxpbmUobGluZXR5cGU9MikgKwogIGdlb21fcG9pbnQoKSArCiAgZ2d0aXRsZSgiUmVmaW5lZCBzYW1wbGluZyIpIApyZXNfcmVmICU+JQogIGdncGxvdChhZXMoLWxvZzEwKFBWYWx1ZSksIC1sb2cxMChTcGF0aWFsRkRSKSkpICsKICBnZW9tX2FibGluZShsaW5ldHlwZT0yKSArCiAgZ2d0aXRsZSgiUmFuZG9tIHNhbXBsaW5nIikgKwogIGdlb21fcG9pbnQoKSAKCmBgYAoKIyMgUGlja2luZyBzYW1wbGluZyBwcm9wb3J0aW9uIGFuZCBrCgpJIHdhbnQgdG8gc2VsZWN0IHRoZXNlIHBhcmFtZXRlcnMgdG8gaW5jcmVhc2UgbWVhbiBuaCBzaXplCgpgYGB7ciwgZmlnLmhlaWdodD02LCBmaWcud2lkdGg9Nn0KdGVzdF9tZWFuX25oX3NpemUgPC0gZnVuY3Rpb24obSwgcHJvcCwgaywgbl9jZWxscywgZD0zMCl7CiAgbSA8LSBtWyxzYW1wbGUoY29sbmFtZXMobSksIHNpemUgPSBuX2NlbGxzKV0KICBtIDwtIGJ1aWxkR3JhcGgobSwgayA9IGssIGQgPSBkKQogIHJlZmluZWRfbmggPC0gbmVpZ2hib3VyaG9vZHMobWFrZU5laWdoYm91cmhvb2RzKG0sIHByb3A9cHJvcCwgaz1rLCBkPWQsIHJlZmluZWQgPSBUUlVFLCBzZWVkPTQyKSkKICByZXR1cm4obWVhbihzYXBwbHkocmVmaW5lZF9uaCwgbGVuZ3RoKSkpCn0KCnByb3BfdmVjIDwtIHNlcSgwLjA1LDAuMjUsIGJ5ID0gMC4wNSkKa192ZWMgPC0gc2VxKDEwLDUwLCBieT0xMCkKbmNlbGxzX3ZlYyA8LSBzZXEoMTAwMCwgNTAwMCwgYnk9MTAwMCkKCmdyaWRfZGYgPC0gZXhwYW5kLmdyaWQobmNlbGxzX3ZlYywga192ZWMsIHByb3BfdmVjKQpjb2xuYW1lcyhncmlkX2RmKSA8LSBjKCJuX2NlbGxzIiwgImsiLCAicHJvcCIpCgptZWFuX25oX3NpemVzIDwtIGFwcGx5KGdyaWRfZGYsIDEsIGZ1bmN0aW9uKHgpIHRlc3RfbWVhbl9uaF9zaXplKG0sIHhbInByb3AiXSwgeFsiayJdLCB4WyJuX2NlbGxzIl0pKQoKCmdyaWRfZGYgJT4lCiAgbXV0YXRlKG1lYW5fbmhfc2l6ZT1tZWFuX25oX3NpemVzKSAlPiUKICBncm91cF9ieShuX2NlbGxzLCBrKSAlPiUKICBzdW1tYXJpc2UobWVhbl9uaF9zaXplPW1lYW4obWVhbl9uaF9zaXplKSkgJT4lCiAgZ2dwbG90KGFlcyhrLCBuX2NlbGxzKSkgKwogICMgZ2VvbV9wb2ludCgpICsKICAjIGdlb21fbGluZShhZXMoZ3JvdXA9bl9jZWxscykpICsKICBnZW9tX3RpbGUoYWVzKGZpbGw9bWVhbl9uaF9zaXplKSkgKwogIHNjYWxlX2ZpbGxfdmlyaWRpc19jKCkgCgoKYGBgCgpUcnkgdXNpbmcgcmVhbCBkYXRhICh0aHltdXMgZGF0YXNldCkKCmBgYHtyfQpzY2UgPC0gcmVhZFJEUygifi9Eb3dubG9hZHMvSFRBMDgudjAxLkEwNi5TY2llbmNlX2h1bWFuX3RjZWxscy5TaW5nbGVDZWxsRXhwZXJpbWVudC5SRFMiKQprZWVwX2NlbGxzIDwtIHdoaWNoKHNjZSRzb3J0ICVpbiUgYygiVE9UIiwgIjQ1UCIpKQpzY2UgPC0gc2NlWyxrZWVwX2NlbGxzXQoKbWlsbyA8LSBNaWxvKHNjZSkKbWlsbwpyZWR1Y2VkRGltKG1pbG8sICJQQ0EiKSA8LSByZWR1Y2VkRGltKG1pbG8pCgoKYGBgCgpgYGB7cn0KIyBwcm9wX3ZlYyA8LSBzZXEoMC4wNSwwLjI1LCBieSA9IDAuMDUpCmtfdmVjIDwtIHNlcSgxMCw1MCwgYnk9NSkKbmNlbGxzX3ZlYyA8LSByb3VuZChuY29sKG1pbG8pKnNlcSgwLjEsMSwgYnk9MC4xKSwwKQoKZ3JpZF9kZiA8LSBleHBhbmQuZ3JpZChuY2VsbHNfdmVjLCBrX3ZlYykKY29sbmFtZXMoZ3JpZF9kZikgPC0gYygibl9jZWxscyIsICJrIikKbWVhbl9uaF9zaXplc190aCA8LSBhcHBseShncmlkX2RmLCAxLCBmdW5jdGlvbih4KSB0ZXN0X21lYW5fbmhfc2l6ZShtaWxvLCBwcm9wID0gMC4yLCB4WyJrIl0sIHhbIm5fY2VsbHMiXSwgZD01KSkKCnRlc3RfbWVhbl9uaF9zaXplKG1pbG8sIHByb3AgPSAwLjIsIGdyaWRfZGZbMSwiayJdLCBncmlkX2RmWzEsIm5fY2VsbHMiXSwgZD01KQpgYGAKCgotLS0KCiMjIE9sZCBjb2RlCgojIyBSb2J1c3RuZXNzIG9mIHRlc3Qgb3V0Y29tZXMKCkkgd2FudCB0byBjaGVjayB3aGV0aGVyIHVzaW5nIHJlZmluZWQgc2FtcGxpbmcgYWxsb3dzIHRvIGhhdmUgbW9yZSBsb2dGQyBldmVuIHdpdGggZGlmZmVyZW50IHNhbXBsaW5nIAoKYGBge3J9CnNwRkRSLnJhbmRvbSRyZXMKaW50ZXJzZWN0KHJhbmRvbS52ZXJ0aWNlcywgcmVmaW5lZC52ZXJ0aWNlcykKYGBgCgoKYGBge3IsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD03fQpydW5fbWlsb19zYW1wbGluZyA8LSBmdW5jdGlvbihncmFwaCwgbWV0YS5kZiwgbW9kZWwsIFhfcGNhLCBzZWVkPTQyLCBzYW1wbGUudmVydGljZXM9MC4xKXsKICBzZXQuc2VlZChzZWVkKQogIHJhbmRvbS52ZXJ0aWNlcyA8LSBzYW1wbGUoVihncmFwaCksIHNpemU9Zmxvb3Ioc2FtcGxlLnZlcnRpY2VzKmxlbmd0aChWKGdyYXBoKSkpKQogIHZlcnRleC5rbm4gPC0gQmlvY05laWdoYm9yczo6ZmluZEtOTihYPVhfcGNhLCBrPTIxLCBzdWJzZXQ9YXMudmVjdG9yKHJhbmRvbS52ZXJ0aWNlcykpCiAgcmVmaW5lZC52ZXJ0aWNlcyA8LSBWKGdyYXBoKVtzYXBwbHkoMTpucm93KHZlcnRleC5rbm4kaW5kZXgpLCBmdW5jdGlvbihpKSByZWZpbmVfdmVydGV4KHZlcnRleC5rbm4sIGksIFhfcGNhKSldCiAgCiAgdmVydGV4Lmxpc3QgPC0gc2FwcGx5KDE6bGVuZ3RoKHJhbmRvbS52ZXJ0aWNlcyksIEZVTj1mdW5jdGlvbihYKSBuZWlnaGJvcnMoZ3JhcGgsIHY9cmFuZG9tLnZlcnRpY2VzW1hdKSkKICB2ZXJ0ZXgubGlzdC5yZWZpbmVkIDwtIHNhcHBseSgxOmxlbmd0aChyZWZpbmVkLnZlcnRpY2VzKSwgRlVOPWZ1bmN0aW9uKFgpIG5laWdoYm9ycyhncmFwaCwgdj1yZWZpbmVkLnZlcnRpY2VzW1hdKSkKICAKICBjb3VudC5tYXRyaXgucmFuZG9tIDwtIGNvdW50Q2VsbHMoc2ltMi5rbm4sIG1ldGEuZGYsIHZlcnRleC5saXN0ID0gdmVydGV4Lmxpc3QsIHJhbmRvbS52ZXJ0aWNlcyA9IHJhbmRvbS52ZXJ0aWNlcywgc2FtcGxlLmNvbHVtbiA9ICJzYW1wbGUiKQogIGNvdW50Lm1hdHJpeC5yZWZpbmVkIDwtIGNvdW50Q2VsbHMoc2ltMi5rbm4sIG1ldGEuZGYsIHZlcnRleC5saXN0ID0gdmVydGV4Lmxpc3QucmVmaW5lZCwgcmFuZG9tLnZlcnRpY2VzID0gcmVmaW5lZC52ZXJ0aWNlcywgc2FtcGxlLmNvbHVtbiA9ICJzYW1wbGUiKQogICAgCiAgc3BGRFIucmFuZG9tIDwtIHRlc3RRTEYoZ3JhcGgsIGNvdW50Lm1hdHJpeC5yYW5kb20sIG1vZGVsKQogIHNwRkRSLnJlZmluZWQgPC0gdGVzdFFMRihncmFwaCwgY291bnQubWF0cml4LnJlZmluZWQsIG1vZGVsKQogIAogIGZkci5kZi5yYW5kb20gPC0gZGF0YS5mcmFtZShWZXJ0ZXg9YXMuaW50ZWdlcihyb3duYW1lcyhzcEZEUi5yYW5kb20kcmVzKSksIHA9c3BGRFIucmFuZG9tJHJlcyRQVmFsdWUsIGFkanA9c3BGRFIucmFuZG9tJHNwRkRSLCBhZGpwX2Zkcj1zcEZEUi5yYW5kb20kcmVzJEZEUiwgbG9nRkM9c3BGRFIucmFuZG9tJHJlcyRsb2dGQywgU2lnPXNwRkRSLnJhbmRvbSRyZXMkU2lnKQogIGZkci5kZi5yZWZpbmVkIDwtIGRhdGEuZnJhbWUoVmVydGV4PWFzLmludGVnZXIocm93bmFtZXMoc3BGRFIucmVmaW5lZCRyZXMpKSwgcD1zcEZEUi5yZWZpbmVkJHJlcyRQVmFsdWUsIGFkanA9c3BGRFIucmVmaW5lZCRzcEZEUiwgbG9nRkM9c3BGRFIucmVmaW5lZCRyZXMkbG9nRkMsIGFkanBfZmRyPXNwRkRSLnJlZmluZWQkcmVzJEZEUiwgU2lnPXNwRkRSLnJlZmluZWQkcmVzJFNpZykKCiAgcmV0dXJuKGxpc3QocmFuZG9tPWZkci5kZi5yYW5kb20sIHJlZmluZWQ9ZmRyLmRmLnJlZmluZWQpKQp9CgpzYW1wbGVfcGVyYzUgPC0gbWFwKDIwMjA6MjAyNSwgfiBydW5fbWlsb19zYW1wbGluZyhkYXRhXzVrX2NlbGxzJGdyYXBoLCBkYXRhXzVrX2NlbGxzJG1ldGEuZGYsIGRhdGFfNWtfY2VsbHMkbW9kZWwsIGRhdGFfNWtfY2VsbHMkWF9wY2EsIHNlZWQ9LngsIHNhbXBsZS52ZXJ0aWNlcyA9IDAuMDUpKQpzYW1wbGVfcGVyYzEwIDwtIG1hcCgyMDIwOjIwMjUsIH4gcnVuX21pbG9fc2FtcGxpbmcoZGF0YV81a19jZWxscyRncmFwaCwgZGF0YV81a19jZWxscyRtZXRhLmRmLCBkYXRhXzVrX2NlbGxzJG1vZGVsLCBkYXRhXzVrX2NlbGxzJFhfcGNhLCBzZWVkPS54LCBzYW1wbGUudmVydGljZXMgPSAwLjEpKQpzYW1wbGVfcGVyYzE1IDwtIG1hcCgyMDIwOjIwMjUsIH4gcnVuX21pbG9fc2FtcGxpbmcoZGF0YV81a19jZWxscyRncmFwaCwgZGF0YV81a19jZWxscyRtZXRhLmRmLCBkYXRhXzVrX2NlbGxzJG1vZGVsLCBkYXRhXzVrX2NlbGxzJFhfcGNhLCBzZWVkPS54LCBzYW1wbGUudmVydGljZXMgPSAwLjE1KSkKc2FtcGxlX3BlcmMyMCA8LSBtYXAoMjAyMDoyMDI1LCB+IHJ1bl9taWxvX3NhbXBsaW5nKGRhdGFfNWtfY2VsbHMkZ3JhcGgsIGRhdGFfNWtfY2VsbHMkbWV0YS5kZiwgZGF0YV81a19jZWxscyRtb2RlbCwgZGF0YV81a19jZWxscyRYX3BjYSwgc2VlZD0ueCwgc2FtcGxlLnZlcnRpY2VzID0gMC4yKSkKCgptYWtlX3Rlc3RfZGYgPC0gZnVuY3Rpb24oc2FtcGxlX2RmKXsKICBzYW1wbGVfZGYgJT4lCiAgaW1hcCggfiBiaW5kX3Jvd3MoLnhbWyJyZWZpbmVkIl1dICU+JSBkcGx5cjo6bXV0YXRlKHNhbXBsaW5nPSJyZWZpbmVkIiksCiAgICAgICAgICAgICAgICAgICAgIC54W1sicmFuZG9tIl1dICU+JSBkcGx5cjo6bXV0YXRlKHNhbXBsaW5nPSJyYW5kb20iKSkgJT4lCiAgICAgICAgICAgZHBseXI6Om11dGF0ZShzPS55KSkgJT4lCiAgICBwdXJycjo6cmVkdWNlKGJpbmRfcm93cykgJT4lCiAgICBsZWZ0X2pvaW4oZGF0YV81a19jZWxscyRtZXRhLmRmKSAlPiUKICAgIGRwbHlyOjptdXRhdGUoZ3JvdXBfaWQgPSBmYWN0b3IoZ3JvdXBfaWQsIGxldmVscz1wYXN0ZTAoJ00nLCAxOm51bV9taWxlc3RvbmVzKSkpICU+JQogICAgZ3JvdXBfYnkoc2FtcGxpbmcsIHMsIGdyb3VwX2lkKSAlPiUKICAgIHN1bW1hcmlzZShtZWFuX2xvZ0ZDPW1lYW4obG9nRkMpKSAKICB9CgptYXAobGlzdChwZXJjNT1zYW1wbGVfcGVyYzUsIHBlcmMxMD1zYW1wbGVfcGVyYzEwLCBwZXJjMTU9c2FtcGxlX3BlcmMxNSwgcGVyYzIwPXNhbXBsZV9wZXJjMjApLCB+IG1ha2VfdGVzdF9kZigueCkpICU+JQogIGltYXAoIH4gZHBseXI6Om11dGF0ZSgueCwgcGVyYz0ueSkpICU+JQogIHB1cnJyOjpyZWR1Y2UoYmluZF9yb3dzKSAlPiUKICBnZ3Bsb3QoYWVzKGdyb3VwX2lkLCBtZWFuX2xvZ0ZDLCBjb2xvcj1wZXJjKSkgKwogICMgZ2VvbV9wb2ludHJhbmdlKHN0YXQgPSAic3VtbWFyeSIsCiAgIyAgIGZ1bi5taW4gPSBtaW4sCiAgIyAgIGZ1bi5tYXggPSBtYXgsCiAgIyAgIGZ1biA9IG1lYW4pICsKICBnZW9tX2JveHBsb3QodmFyd2lkdGggPSBUUlVFKSArCiAgZmFjZXRfZ3JpZCgufnNhbXBsaW5nKSArCiAgc2NhbGVfZmlsbF9ncmFkaWVudDIoKQpgYGAKCk5vIGJpZyBkaWZmZXJlbmNlcyBUQkgKCi0tLQoKIyMgQ29tcG9zaXRpb25hbCBlZmZlY3QKCkRvIEkgZ2V0IGhpZ2gvbG93IEZDIHdoZXJlIHVuZXhwZWN0ZWQganVzdCBiZWNhdXNlIHRoaW5ncyBhcmUgY2hhbmdpbmcgZWxzZXdoZXJlPwoKCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQpkYXRhXzJrX2NlbGxzIDwtIHNpbXVsYXRlX2xpbmVhcl90cmFqKG51bV9jZWxscyA9IDIwMDAsIG51bV9taWxlc3RvbmVzID0gMTAsIHByb2Jfc3RhcnQgPSAwLjUsIHByb2JfZW5kPTAuOTUpCmBgYAoKYGBge3J9CmdncGxvdChkYXRhXzJrX2NlbGxzJG1ldGEuZGYsIGFlcyhVTUFQMSwgVU1BUDIsIGNvbG9yPWNvbmRpdGlvbikpICsgZ2VvbV9wb2ludChzaXplPTAuMikgKwogIHRoZW1lX2NsZWFuKCkgCmdncGxvdChkYXRhXzJrX2NlbGxzJG1ldGEuZGYsIGFlcyhVTUFQMSwgVU1BUDIsIGNvbG9yPWdyb3VwX2lkKSkgKyBnZW9tX3BvaW50KHNpemU9MC4yKSArCiAgdGhlbWVfY2xlYW4oKSArCiAgZ2VvbV90ZXh0KGRhdGEgPSAuICU+JSBncm91cF9ieShncm91cF9pZCkgJT4lIHN1bW1hcmlzZShVTUFQMT1maXJzdChVTUFQMSksIFVNQVAyPWZpcnN0KFVNQVAyKSksIGFlcyhsYWJlbD1ncm91cF9pZCksIGNvbG9yPSJibGFjayIpCmBgYAoKYGBge3J9CgpncmFwaCA8LSBkYXRhXzJrX2NlbGxzJGdyYXBoCnNhbXBsZS52ZXJ0aWNlcyA8LSAwLjEKbWV0YS5kZiA8LSBkYXRhXzJrX2NlbGxzJG1ldGEuZGYKbW9kZWwgPC0gZGF0YV8ya19jZWxscyRtb2RlbApYX3BjYSA8LSBkYXRhXzJrX2NlbGxzJFhfcGNhCgpyYW5kb20udmVydGljZXMgPC0gc2FtcGxlKFYoZ3JhcGgpLCBzaXplPWZsb29yKHNhbXBsZS52ZXJ0aWNlcypsZW5ndGgoVihncmFwaCkpKSkKdmVydGV4LmtubiA8LSBCaW9jTmVpZ2hib3JzOjpmaW5kS05OKFg9WF9wY2EsIGs9MjEsIHN1YnNldD1hcy52ZWN0b3IocmFuZG9tLnZlcnRpY2VzKSkKcmVmaW5lZC52ZXJ0aWNlcyA8LSBWKGdyYXBoKVtzYXBwbHkoMTpucm93KHZlcnRleC5rbm4kaW5kZXgpLCBmdW5jdGlvbihpKSByZWZpbmVfdmVydGV4KHZlcnRleC5rbm4sIGksIFhfcGNhKSldCgp2ZXJ0ZXgubGlzdCA8LSBzYXBwbHkoMTpsZW5ndGgocmFuZG9tLnZlcnRpY2VzKSwgRlVOPWZ1bmN0aW9uKFgpIG5laWdoYm9ycyhncmFwaCwgdj1yYW5kb20udmVydGljZXNbWF0pKQp2ZXJ0ZXgubGlzdC5yZWZpbmVkIDwtIHNhcHBseSgxOmxlbmd0aChyZWZpbmVkLnZlcnRpY2VzKSwgRlVOPWZ1bmN0aW9uKFgpIG5laWdoYm9ycyhncmFwaCwgdj1yZWZpbmVkLnZlcnRpY2VzW1hdKSkKCmNvdW50Lm1hdHJpeC5yYW5kb20gPC0gY291bnRDZWxscyhncmFwaCwgbWV0YS5kZiwgdmVydGV4Lmxpc3QgPSB2ZXJ0ZXgubGlzdCwgcmFuZG9tLnZlcnRpY2VzID0gcmFuZG9tLnZlcnRpY2VzLCBzYW1wbGUuY29sdW1uID0gInNhbXBsZSIpCmNvdW50Lm1hdHJpeC5yZWZpbmVkIDwtIGNvdW50Q2VsbHMoZ3JhcGgsIG1ldGEuZGYsIHZlcnRleC5saXN0ID0gdmVydGV4Lmxpc3QucmVmaW5lZCwgcmFuZG9tLnZlcnRpY2VzID0gcmVmaW5lZC52ZXJ0aWNlcywgc2FtcGxlLmNvbHVtbiA9ICJzYW1wbGUiKQogIApzcEZEUi5yYW5kb20gPC0gdGVzdFFMRihncmFwaCwgY291bnQubWF0cml4LnJhbmRvbSwgbW9kZWwpCnNwRkRSLnJlZmluZWQgPC0gdGVzdFFMRihncmFwaCwgY291bnQubWF0cml4LnJlZmluZWQsIG1vZGVsKQoKYGBgCgpSZWZpbmVkIHNhbXBsaW5nIHNlZW1zIHRvIGJlIGFibGUgdG8gaWRlbnRpZnkgREEgYXQgYm90aCBlbmRzIG9mIHRoZSBzcGVjdHJ1bSBiZXR0ZXIKCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpmZHIuZGYucmFuZG9tIDwtIGRhdGEuZnJhbWUoVmVydGV4PWFzLmludGVnZXIocm93bmFtZXMoc3BGRFIucmFuZG9tJHJlcykpLCBwPXNwRkRSLnJhbmRvbSRyZXMkUFZhbHVlLCBhZGpwPXNwRkRSLnJhbmRvbSRzcEZEUiwgYWRqcF9mZHI9c3BGRFIucmFuZG9tJHJlcyRGRFIsIGxvZ0ZDPXNwRkRSLnJhbmRvbSRyZXMkbG9nRkMsIFNpZz1zcEZEUi5yYW5kb20kcmVzJFNpZykKZmRyLmRmLnJlZmluZWQgPC0gZGF0YS5mcmFtZShWZXJ0ZXg9YXMuaW50ZWdlcihyb3duYW1lcyhzcEZEUi5yZWZpbmVkJHJlcykpLCBwPXNwRkRSLnJlZmluZWQkcmVzJFBWYWx1ZSwgYWRqcD1zcEZEUi5yZWZpbmVkJHNwRkRSLCBsb2dGQz1zcEZEUi5yZWZpbmVkJHJlcyRsb2dGQywgYWRqcF9mZHI9c3BGRFIucmVmaW5lZCRyZXMkRkRSLCBTaWc9c3BGRFIucmVmaW5lZCRyZXMkU2lnKQoKbWV0YS5kZiAlPiUKICBsZWZ0X2pvaW4oZmRyLmRmLnJhbmRvbSkgJT4lCiAgIyBkcGx5cjo6YXJyYW5nZShzYW1wbGVkKSAlPiUKICBnZ3Bsb3QoYWVzKFVNQVAxLCBVTUFQMiwgCiAgICAgICAgICAgICAjIGNvbG9yPSAtIGxvZzEwKGFkanApLAogICAgICAgICAgICAjIGNvbG9yPSAtIGxvZzEwKHApLAogICAgICAgICAgICAgY29sb3IgPSBsb2dGQwogICAgICAgICAgICAgKSkgKwogIGdlb21fcG9pbnQoc2l6ZT0wLjUpICsKICBnZW9tX3BvaW50KGRhdGE9LiAlPiUgZHBseXI6OmZpbHRlcighaXMubmEoYWRqcCkpKSArCiAgdGhlbWVfY2xlYW4oKSArCiAgc2NhbGVfY29sb3JfZ3JhZGllbnQyKG1pZHBvaW50ID0gMCwgaGlnaCA9ICJyZWQiLCBsb3c9ImJsdWUiLG5hLnZhbHVlID0iZ3JleTgwIikgKwogIGdndGl0bGUoIlJhbmRvbSBzYW1wbGluZyIpCgoKbWV0YS5kZiAlPiUKICBsZWZ0X2pvaW4oZmRyLmRmLnJlZmluZWQpICU+JQogICMgZHBseXI6OmFycmFuZ2Uoc2FtcGxlZCkgJT4lCiAgZ2dwbG90KGFlcyhVTUFQMSwgVU1BUDIsIAogICAgICAgICAgICAgIyBjb2xvcj0gLSBsb2cxMChhZGpwKSwKICAgICAgICAgICAgIyBjb2xvcj0gLSBsb2cxMChwKSwKICAgICAgICAgICAgIGNvbG9yID0gbG9nRkMKICAgICAgICAgICAgICkpICsKICBnZW9tX3BvaW50KHNpemU9MC41KSArCiAgZ2VvbV9wb2ludChkYXRhPS4gJT4lIGRwbHlyOjpmaWx0ZXIoIWlzLm5hKGFkanApKSkgKwogIHRoZW1lX2NsZWFuKCkgKwogIHNjYWxlX2NvbG9yX2dyYWRpZW50MihtaWRwb2ludCA9IDAsIGhpZ2ggPSAicmVkIiwgbG93PSJibHVlIixuYS52YWx1ZSA9ImdyZXk4MCIpICsKICBnZ3RpdGxlKCJSZWZpbmVkIHNhbXBsaW5nIikKCmBgYAoKCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBmaWcud2lkdGg9MTAsIGZpZy5oZWlnaHQ9NX0KbWV0YS5kZiAlPiUKICAgbGVmdF9qb2luKGZkci5kZi5yZWZpbmVkKSAlPiUKICBkcGx5cjo6ZmlsdGVyKCFpcy5uYShsb2dGQykpICU+JQogIGdncGxvdChhZXMobG9nRkMsIC1sb2cxMChhZGpwKSwgc2hhcGU9U2lnLCBjb2xvcj1ncm91cF9pZCkpICsKICBnZW9tX3BvaW50KCkgKwogIGdndGl0bGUoInJlZmluZWQgc2FtcGxpbmciKSArCm1ldGEuZGYgJT4lCiAgIGxlZnRfam9pbihmZHIuZGYucmFuZG9tKSAlPiUKICBkcGx5cjo6ZmlsdGVyKCFpcy5uYShsb2dGQykpICU+JQogIGdncGxvdChhZXMobG9nRkMsIC1sb2cxMChhZGpwKSwgc2hhcGU9U2lnLCBjb2xvcj1ncm91cF9pZCkpICsKICBnZW9tX3BvaW50KCkgKwogIGdndGl0bGUoInJhbmRvbSBzYW1wbGluZyIpIApgYGAKCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBmaWcud2lkdGg9MTIsIGZpZy5oZWlnaHQ9NX0KbnVtX21pbGVzdG9uZXM9MTAKCm1ldGEuZGYgJT4lCiAgdW5ncm91cCgpICU+JQogIGxlZnRfam9pbihmZHIuZGYucmVmaW5lZCkgJT4lCiAgZHBseXI6Om11dGF0ZShncm91cF9pZCA9IGZhY3Rvcihncm91cF9pZCwgbGV2ZWxzPXBhc3RlMCgnTScsIDE6bnVtX21pbGVzdG9uZXMpKSkgJT4lCiAgZHBseXI6OmZpbHRlcighaXMubmEobG9nRkMpKSAlPiUKICBnZ3Bsb3QoYWVzKGdyb3VwX2lkLCBsb2dGQywgIHNoYXBlPVNpZywgY29sb3I9Z3JvdXBfaWQsIHNpemU9IC1sb2cxMChhZGpwKSkpICsKICBnZW9tX2ppdHRlcigpICsKICBnZ3RpdGxlKCJyZWZpbmVkIHNhbXBsaW5nIikgKwptZXRhLmRmICU+JQogIHVuZ3JvdXAoKSAlPiUKICBsZWZ0X2pvaW4oZmRyLmRmLnJhbmRvbSkgJT4lCiAgZHBseXI6Om11dGF0ZShncm91cF9pZCA9IGZhY3Rvcihncm91cF9pZCwgbGV2ZWxzPXBhc3RlMCgnTScsIDE6bnVtX21pbGVzdG9uZXMpKSkgJT4lICBkcGx5cjo6ZmlsdGVyKCFpcy5uYShsb2dGQykpICU+JQogIGdncGxvdChhZXMoZ3JvdXBfaWQsIGxvZ0ZDLCBzaGFwZT1TaWcsIGNvbG9yPWdyb3VwX2lkLCBzaXplPSAtbG9nMTAoYWRqcCkpKSArCiAgZ2VvbV9qaXR0ZXIoKSArCiAgZ2d0aXRsZSgicmFuZG9tIHNhbXBsaW5nIikgCmBgYAoKLS0tCgojIyBIb3cgbWFueSBuZWlnaGJvcmhvb2RzIGRpc2FwcGVhciB3IHJlZmluZW1lbnQ/CgpgYGB7cn0Kc2ltdWxhdGVfbGluZWFyX3RyYWogPC0gZnVuY3Rpb24obnVtX2NlbGxzLCBudW1fbWlsZXN0b25lcywgbnVtX2ZlYXR1cmVzPTEwMDAsIGtfcGFyYW09MjEsIHNlZWQ9NDIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHByb2Jfc3RhcnQ9MC4xLCBwcm9iX2VuZD0wLjkpewogIHNldC5zZWVkKHNlZWQpCiAgIyMgR2VuZXJhdGUgc2ltdWxhdGVkIGRhdGFzZXQgb2YgdHJhamVjdG9yeQogIGRhdGFzZXQgPC0gZ2VuZXJhdGVfZGF0YXNldCgKICAgIG1vZGVsID0gbW9kZWxfbGluZWFyKG51bV9taWxlc3RvbmVzID0gbnVtX21pbGVzdG9uZXMpLAogICAgbnVtX2NlbGxzID0gbnVtX2NlbGxzLAogICAgbnVtX2ZlYXR1cmVzID0gbnVtX2ZlYXR1cmVzCiAgKQogIHNpbTIuZ2V4IDwtIGFzLm1hdHJpeChkYXRhc2V0JGV4cHJlc3Npb24pCiAgc2ltMi5icmFuY2hlcyA8LSBkYXRhc2V0JHByaW9yX2luZm9ybWF0aW9uJGdyb3Vwc19pZAogIHNpbTIudGltZSA9IGRhdGFzZXQkcHJpb3JfaW5mb3JtYXRpb24kdGltZWNvdXJzZV9jb250aW51b3VzCiAgCiAgIyMgQnVpbGQgZ3JhcGggCiAgc2ltMi5wY2EgPC0gcHJjb21wX2lybGJhKHNpbTIuZ2V4LCBuPTUwLCBzY2FsZS49VFJVRSwgY2VudGVyPVRSVUUpCiAgWF9wY2EgPSBzaW0yLnBjYSR4WywgYygxOjMwKV0KICBzaW0yLmtubiA8LSBidWlsZEtOTkdyYXBoKHg9WF9wY2EsIGs9a19wYXJhbSwgZD1OQSwgdHJhbnNwb3NlZD1UUlVFKQogICMjIFJ1biBVTUFQCiAgc3RlbS50YS51bWFwIDwtIHVtYXAoc2ltMi5wY2EkeFssIGMoMTozMCldLAogICAgICAgICAgICAgICAgICAgICAgIG5fY29tcG9uZW50cz0yLAogICAgICAgICAgICAgICAgICAgICAgIG5fbmVpZ2hib3JzPWtfcGFyYW0sIG1ldHJpYz0nZXVjbGlkZWFuJywKICAgICAgICAgICAgICAgICAgICAgICBpbml0PSdyYW5kb20nLCBtaW5fZGlzdD0wLjEpCiAgZHluLmRmIDwtIGRhdGEuZnJhbWUoVU1BUDE9c3RlbS50YS51bWFwJGxheW91dFssMV0sIFVNQVAyPXN0ZW0udGEudW1hcCRsYXlvdXRbLDJdLCAKICAgICAgICAgICAgIGNlbGxfaWQ9cm93bmFtZXMoc2ltMi5nZXgpLCB0aW1lPXNpbTIudGltZSkKICBkeW4uZGYgPC0gZHluLmRmICU+JSBsZWZ0X2pvaW4oc2ltMi5icmFuY2hlcykKICAKICAjIyBTaW11bGF0ZSBjb25kaXRpb25zCiAgbl9ncm91cHMgPC0gbGVuZ3RoKHVuaXF1ZShkeW4uZGYkZ3JvdXBfaWQpKQogIHBfdmVjIDwtIHNlcShwcm9iX3N0YXJ0LCBwcm9iX2VuZCwgbGVuZ3RoLm91dCA9IG5fZ3JvdXBzKQogIGEuY2VsbHMgPC0gYygpCiAgZm9yIChpIGluIDE6bl9ncm91cHMpIHsKICAgIGcgPC0gcGFzdGUwKCJNIixpKQogICAgcCA8LSBwX3ZlY1tpXSAKICAgIG0uQSA8LSBzYW1wbGUoZHluLmRmJGNlbGxfaWRbZHluLmRmJGdyb3VwX2lkPT1nXSwgCiAgICAgICAgICAgICAgICAgIHNpemU9Zmxvb3Ioc3VtKGR5bi5kZiRncm91cF9pZD09ZykqcCkpCiAgICBhLmNlbGxzIDwtIGMoYS5jZWxscywgbS5BKQogIH0KICAKICBkeW4uZGYgPC0gZHluLmRmICU+JSBkcGx5cjo6bXV0YXRlKGNvbmRpdGlvbiA9IGlmZWxzZShjZWxsX2lkICVpbiUgYS5jZWxscywgIkEiLCAnQicpKSAKCiAgIyMgU2ltdWxhdGUgcmVwbGljYXRlcwogIGR5bi5kZiA8LSBkeW4uZGYgJT4lCiAgICBncm91cF9ieShncm91cF9pZCkgJT4lCiAgICBkcGx5cjo6bXV0YXRlKHJlcGxpY2F0ZT1jKHJlcCgiUjEiLCBmbG9vcihuKCkqMC4zKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXAoIlIyIiwgZmxvb3IobigpKjAuMykpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwKCJSMyIsIG4oKSAtIDIqKGZsb29yKG4oKSowLjMpKSkpCiAgICApIAogIAogICMjIEFkZCBzYW1wbGUgbmFtZSAoY29uZGl0aW9uICsgcmVwbGljYXRlKQogIGR5bi5kZiRzYW1wbGUgPC0gcGFzdGUoZHluLmRmJGNvbmRpdGlvbiwgZHluLmRmJHJlcGxpY2F0ZSwgc2VwPSJfIikKICAjIyBBZGQgdmVydGV4IGlkIChmb3IgY291bnRzKQogIGR5bi5kZiRWZXJ0ZXggPC0gYXMudmVjdG9yKFYoc2ltMi5rbm4pKQogIAogICMjIE1ha2UgbW9kZWwgbWF0cml4IGZvciB0ZXN0aW5nCiAgc2FtcGxlLm1ldGEgPC0gZGF0YS5mcmFtZSgiQ29uZGl0aW9uIj1jKHJlcCgiQSIsIDMpLCByZXAoIkIiLCAzKSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAiUmVwbGljYXRlIj1yZXAoYygiUjEiLCAiUjIiLCAiUjMiKSwgMikpCiAgc2FtcGxlLm1ldGEkU2FtcGxlIDwtIHBhc3RlKHNhbXBsZS5tZXRhJENvbmRpdGlvbiwgc2FtcGxlLm1ldGEkUmVwbGljYXRlLCBzZXA9Il8iKQogIHJvd25hbWVzKHNhbXBsZS5tZXRhKSA8LSBzYW1wbGUubWV0YSRTYW1wbGUKICBzaW0yLm1vZGVsIDwtIG1vZGVsLm1hdHJpeCh+IDAgKyBDb25kaXRpb24sIGRhdGE9c2FtcGxlLm1ldGEpCiAgCiAgcmV0dXJuKGxpc3QoZ3JhcGg9c2ltMi5rbm4sCiAgICAgICAgICAgICAgWF9wY2E9WF9wY2EsCiAgICAgICAgICAgICAgbWV0YS5kZj1keW4uZGYsCiAgICAgICAgICAgICAgbW9kZWw9c2ltMi5tb2RlbCkpCiAgCiAgfQpgYGAKCgpgYGB7ciwgZmlnLndpZHRoPTEyLCBmaWcuaGVpZ2h0PTV9CmRhdGFfMmtfY2VsbHMgPC0gc2ltdWxhdGVfbGluZWFyX3RyYWoobnVtX2NlbGxzID0gMjAwMCwgbnVtX21pbGVzdG9uZXMgPSAxMCwgcHJvYl9zdGFydCA9IDAuNSwgcHJvYl9lbmQ9MC45NSkKCmdyYXBoIDwtIGRhdGFfMmtfY2VsbHMkZ3JhcGgKc2FtcGxlLnZlcnRpY2VzIDwtIDAuMQptZXRhLmRmIDwtIGRhdGFfMmtfY2VsbHMkbWV0YS5kZgptb2RlbCA8LSBkYXRhXzJrX2NlbGxzJG1vZGVsClhfcGNhIDwtIGRhdGFfMmtfY2VsbHMkWF9wY2EKCnJhbmRvbS52ZXJ0aWNlcyA8LSBzYW1wbGUoVihncmFwaCksIHNpemU9Zmxvb3Ioc2FtcGxlLnZlcnRpY2VzKmxlbmd0aChWKGdyYXBoKSkpKQp2ZXJ0ZXgua25uIDwtIEJpb2NOZWlnaGJvcnM6OmZpbmRLTk4oWD1YX3BjYSwgaz0yMSwgc3Vic2V0PWFzLnZlY3RvcihyYW5kb20udmVydGljZXMpKQpyZWZpbmVkLnZlcnRpY2VzIDwtIFYoZ3JhcGgpW3NhcHBseSgxOm5yb3codmVydGV4LmtubiRpbmRleCksIGZ1bmN0aW9uKGkpIHJlZmluZV92ZXJ0ZXgodmVydGV4LmtubiwgaSwgWF9wY2EpKV0KCgpkYXRhLmZyYW1lKHJhbmRvbT1hcy5udW1lcmljKHJhbmRvbS52ZXJ0aWNlcyksIHJlZmluZWQ9YXMubnVtZXJpYyhyZWZpbmVkLnZlcnRpY2VzKSkgJT4lCiAgcm93aWRfdG9fY29sdW1uKCkgJT4lCiAgZ3JvdXBfYnkoYXMuZmFjdG9yKHJlZmluZWQpKSAlPiUKICBkcGx5cjo6bXV0YXRlKG5fY29udmVyZ2luZyA9IG4oKSkgJT4lCiAgdW5ncm91cCgpICU+JQogIHBpdm90X2xvbmdlcihjb2xzPWMoJ3JhbmRvbScsICJyZWZpbmVkIiksIG5hbWVzX3RvID0gInNhbXBsaW5nX3NjaGVtZSIsIHZhbHVlc190byA9ICJWZXJ0ZXgiKSAlPiUKICBsZWZ0X2pvaW4obWV0YS5kZiwgYnk9IlZlcnRleCIpICU+JQogIGdncGxvdChhZXModGltZSxzYW1wbGluZ19zY2hlbWUsIGNvbG9yPW5fY29udmVyZ2luZykpICsKICBnZW9tX3BvaW50KHNpemU9MC41KSArCiAgZ2VvbV9saW5lKGFlcyhncm91cD1yb3dpZCksIHNpemU9MC41KSArCiAgc2NhbGVfY29sb3JfdmlyaWRpc19jKCkKCmBgYApgYGB7cn0KZGF0YS5mcmFtZShyYW5kb209YXMubnVtZXJpYyhyYW5kb20udmVydGljZXMpLCByZWZpbmVkPWFzLm51bWVyaWMocmVmaW5lZC52ZXJ0aWNlcykpICU+JQogIHJvd2lkX3RvX2NvbHVtbigpICU+JQogIGdyb3VwX2J5KGFzLmZhY3RvcihyZWZpbmVkKSkgJT4lCiAgZHBseXI6Om11dGF0ZShuX2NvbnZlcmdpbmcgPSBuKCkpICU+JQogIGdncGxvdChhZXMoYXMuZmFjdG9yKG5fY29udmVyZ2luZykpKSArIGdlb21faGlzdG9ncmFtKHN0YXQ9ImNvdW50IikKYGBgCgojIyMgQWZ0ZXIgcmVmaW5lbWVudCB3aGF0IGlzIHRoZSBkaXN0YW5jZSB0byB0aGUgbmVhcmVzdCBzYW1wbGVkIGNlbGw/IApEaXN0YW5jZXMgc2hvdWxkIGJlY29tZSBtb3JlIHVuaWZvcm0KCmBgYHtyLCBmaWcud2lkdGg9MTIsIGZpZy5oZWlnaHQ9NX0KZ2V0X2Rpc3RfdG9fY2xvc2VzdF9uZWlnaCA8LSBmdW5jdGlvbihncmFwaCwgc2FtcGxlLnZlcnRpY2VzKXsKICByYW5kb20udmVydGljZXMgPC0gc2FtcGxlKFYoZ3JhcGgpLCBzaXplPWZsb29yKHNhbXBsZS52ZXJ0aWNlcypsZW5ndGgoVihncmFwaCkpKSkKICB2ZXJ0ZXgua25uIDwtIEJpb2NOZWlnaGJvcnM6OmZpbmRLTk4oWD1YX3BjYSwgaz0yMSwgc3Vic2V0PWFzLnZlY3RvcihyYW5kb20udmVydGljZXMpKQogIHJlZmluZWQudmVydGljZXMgPC0gVihncmFwaClbc2FwcGx5KDE6bnJvdyh2ZXJ0ZXgua25uJGluZGV4KSwgZnVuY3Rpb24oaSkgcmVmaW5lX3ZlcnRleCh2ZXJ0ZXgua25uLCBpLCBYX3BjYSkpXQogIGRpc3RfdG9fY2xvc2VzdF9yYW5kb20gPC0gQmlvY05laWdoYm9yczo6ZmluZEtOTihYPVhfcGNhW2FzLnZlY3RvcihyYW5kb20udmVydGljZXMpLF0sIGs9MSlbWyJkaXN0YW5jZSJdXQogIGRpc3RfdG9fY2xvc2VzdF9yZWZpbmVkIDwtIEJpb2NOZWlnaGJvcnM6OmZpbmRLTk4oWD1YX3BjYVt1bmlxdWUoYXMudmVjdG9yKHJlZmluZWQudmVydGljZXMpKSxdLCBrPTEpW1siZGlzdGFuY2UiXV0KICBkaXN0X2RmIDwtIGJpbmRfcm93cyhkYXRhLmZyYW1lKGRpc3RhbmNlX3RvX2Nsb3Nlc3Q9ZGlzdF90b19jbG9zZXN0X3JlZmluZWQsIHNhbXBsaW5nX3NjaGVtZT0ncmVmaW5lZCcpLAogICAgICAgICAgZGF0YS5mcmFtZShkaXN0YW5jZV90b19jbG9zZXN0PWRpc3RfdG9fY2xvc2VzdF9yYW5kb20sIHNhbXBsaW5nX3NjaGVtZT0ncmFuZG9tJykpICU+JQogICAgZHBseXI6Om11dGF0ZShzYW1wbGVfcGVyYz1zYW1wbGUudmVydGljZXMpCn0KCmRpc3RfbHMgPC0gbWFwKHNlcSgwLjEsMC42LCBieSA9IDAuMDUpLCB+IGdldF9kaXN0X3RvX2Nsb3Nlc3RfbmVpZ2goZ3JhcGgsIC54KSkgCnB1cnJyOjpyZWR1Y2UoZGlzdF9scywgYmluZF9yb3dzKSAlPiUKICBnZ3Bsb3QoYWVzKGFzLmZhY3RvcihzYW1wbGVfcGVyYyksIGRpc3RhbmNlX3RvX2Nsb3Nlc3QsIGNvbG9yPXNhbXBsaW5nX3NjaGVtZSkpICsKICAjIGdnYmVlc3dhcm06Omdlb21fcXVhc2lyYW5kb20oKQogIGdlb21fYm94cGxvdCh2YXJ3aWR0aCA9IFRSVUUpICsKICB4bGFiKCIlIHNhbXBsZWQiKSArIHlsYWIoIkRpc3RhbmNlIHRvIGNsb3Nlc3Qgc2FtcGxlIikgKwogIHRoZW1lX2dyZXkoYmFzZV9zaXplID0gMTQpCmBgYAoKIyMgV2hhdCBpcyB0aGUgcmVsYXRpb25zaGlwIGJldHdlZW4gbmVpZ2hib3Job29kIHNpemUgYW5kIGs/CgpgYGB7cn0KZGF0YV81a19jZWxscyA8LSBzaW11bGF0ZV9saW5lYXJfdHJhaihudW1fY2VsbHMgPSA1MDAwLCBudW1fbWlsZXN0b25lcyA9IDEwKQpgYGAKCmBgYHtyfQptZXRhLmRmIDwtIGRhdGFfNWtfY2VsbHMkbWV0YS5kZgptb2RlbCA8LSBkYXRhXzVrX2NlbGxzJG1vZGVsClhfcGNhIDwtIGRhdGFfNWtfY2VsbHMkWF9wY2EKCmtfdmVjIDwtIHNlcSgxMCw1MCwgYnk9NSkKZ3JhcGhfbHMgPC0gbWFwKGtfdmVjLCB+IGJ1aWxkS05OR3JhcGgoeD1YX3BjYSwgaz0ueCwgZD1OQSwgdHJhbnNwb3NlZD1UUlVFKSkKYGBgCgoKYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0UsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD01fQpnZXRfbmVpZ2hfZGYgPC0gZnVuY3Rpb24oc2FtcGxlZF92ZXJ0aWNlcywgZ3JhcGgsIFhfcGNhLCBrX3BhcmFtLCBzYW1wbGluZ19tb2RlPSJyYW5kb20iKXsKICBpZiAoc2FtcGxpbmdfbW9kZT09InJlZmluZWQiKSB7CiAgICB2ZXJ0ZXgua25uIDwtIEJpb2NOZWlnaGJvcnM6OmZpbmRLTk4oWD1YX3BjYSwgaz1rX3BhcmFtLCBzdWJzZXQ9YXMudmVjdG9yKHNhbXBsZWRfdmVydGljZXMpKQogICAgc2FtcGxlZF92ZXJ0aWNlcyA8LSBWKGdyYXBoKVtzYXBwbHkoMTpucm93KHZlcnRleC5rbm4kaW5kZXgpLCBmdW5jdGlvbihpKSByZWZpbmVfdmVydGV4KHZlcnRleC5rbm4sIGksIFhfcGNhKSldCiAgfQogIHNhbXBsZWRfdmVydGljZXMgPC0gdW5pcXVlKHNhbXBsZWRfdmVydGljZXMpCiAgdmVydGV4Lmxpc3QgPC0gc2FwcGx5KDE6bGVuZ3RoKHNhbXBsZWRfdmVydGljZXMpLCBGVU49ZnVuY3Rpb24oWCkgbmVpZ2hib3JzKGdyYXBoLCB2PXNhbXBsZWRfdmVydGljZXNbWF0pKQogIG5laWdoX2RmIDwtIGRhdGEuZnJhbWUobmVpZ2hfdmVydGV4PWFzLnZlY3RvcihzYW1wbGVkX3ZlcnRpY2VzKSwgbmVpZ2hfc2l6ZT1zYXBwbHkodmVydGV4Lmxpc3QsIGZ1bmN0aW9uKHgpIGxlbmd0aCh4KSksIAogICAgICAgICAgICAgICAgICAgICAgICAgc2FtcGxpbmdfbW9kZT1zYW1wbGluZ19tb2RlLCBrPWtfcGFyYW0pCiAgcmV0dXJuKG5laWdoX2RmKQogIH0KCm5laWdoX2RmX2xzIDwtIGxhcHBseShzZXFfYWxvbmcoa192ZWMpLCBmdW5jdGlvbihpKXsKICByYW5kb21fc2FtcGxlIDwtIHNhbXBsZShWKGdyYXBoX2xzW1tpXV0pLCBzaXplPWZsb29yKHNhbXBsZS52ZXJ0aWNlcypsZW5ndGgoVihncmFwaF9sc1tbaV1dKSkpKQogIHNhbXBsZWRfdmVydGljZXMgPC0gcmFuZG9tX3NhbXBsZQogIHJhbmRvbV9uZWlnaF9kZiA8LSBnZXRfbmVpZ2hfZGYocmFuZG9tX3NhbXBsZSwgZ3JhcGhfbHNbW2ldXSwgWF9wY2EsIGtfdmVjW2ldLCBzYW1wbGluZ19tb2RlPSJyYW5kb20iKQogIHJlZmluZWRfbmVpZ2hfZGYgPC0gZ2V0X25laWdoX2RmKHJhbmRvbV9zYW1wbGUsIGdyYXBoX2xzW1tpXV0sIFhfcGNhLCBrX3ZlY1tpXSwgc2FtcGxpbmdfbW9kZT0icmVmaW5lZCIpCiAgYmluZF9yb3dzKHJhbmRvbV9uZWlnaF9kZiwgcmVmaW5lZF9uZWlnaF9kZikKICB9KQoKcHVycnI6OnJlZHVjZShuZWlnaF9kZl9scywgYmluZF9yb3dzKSAlPiUKICBnZ3Bsb3QoYWVzKGFzLmZhY3RvcihrKSwgbmVpZ2hfc2l6ZSwgY29sb3I9c2FtcGxpbmdfbW9kZSkpICsKICBnZW9tX3Zpb2xpbihzY2FsZSA9ICJ3aWR0aCIpICsKICBnZW9tX2JveHBsb3Qod2lkdGg9MC4yKSArCiAgZmFjZXRfd3JhcChzYW1wbGluZ19tb2Rlfi4pICsKICB4bGFiKCJLIikgKyB5bGFiKCJOZWlnaGJvcmhvb2Qgc2l6ZSIpICsKICB0aGVtZV9jbGVhbihiYXNlX3NpemUgPSAxOCkKCmBgYApgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRSwgZmlnLndpZHRoPTEwLCBmaWcuaGVpZ2h0PTV9CnB1cnJyOjpyZWR1Y2UobmVpZ2hfZGZfbHMsIGJpbmRfcm93cykgJT4lCiAgZ2dwbG90KGFlcyhhcy5mYWN0b3IoayksIG5laWdoX3NpemUgLyBrLCBjb2xvcj1zYW1wbGluZ19tb2RlKSkgKwogIGdlb21fdmlvbGluKHNjYWxlID0gIndpZHRoIikgKwogIGdlb21fYm94cGxvdCh3aWR0aD0wLjIpICsKICBmYWNldF93cmFwKHNhbXBsaW5nX21vZGV+LikgKwogIHhsYWIoIksiKSArIHlsYWIoIk5laWdoYm9yaG9vZCBzaXplIC8gSyIpICsKICB0aGVtZV9jbGVhbihiYXNlX3NpemUgPSAxOCkKCmBgYAoKIyMjIyBSZWxhdGlvbnNoaXAgYmV0d2VlbiBLIGFuZCBuZWlnaGJvcmhvb2Qgc2l6ZQpgYGB7cn0KcHVycnI6OnJlZHVjZShuZWlnaF9kZl9scywgYmluZF9yb3dzKSAlPiUKICBncm91cF9ieShzYW1wbGluZ19tb2RlLCBrKSAlPiUKICBzdW1tYXJpc2Uobj1uKCkpICU+JQogIGdncGxvdChhZXMoayxuLCBjb2xvcj1zYW1wbGluZ19tb2RlKSkgKwogIGdlb21fcG9pbnQoKQpgYGAKCgoKCgoKCgo=